@ -40,9 +40,14 @@ import androidx.lifecycle.OnLifecycleEvent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager ;
import androidx.localbroadcastmanager.content.LocalBroadcastManager ;
import androidx.preference.PreferenceManager ;
import androidx.preference.PreferenceManager ;
import com.android.billingclient.api.AcknowledgePurchaseParams ;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener ;
import com.android.billingclient.api.BillingClient ;
import com.android.billingclient.api.BillingClient ;
import com.android.billingclient.api.BillingClientStateListener ;
import com.android.billingclient.api.BillingClientStateListener ;
import com.android.billingclient.api.BillingFlowParams ;
import com.android.billingclient.api.BillingFlowParams ;
import com.android.billingclient.api.BillingResult ;
import com.android.billingclient.api.ConsumeParams ;
import com.android.billingclient.api.ConsumeResponseListener ;
import com.android.billingclient.api.Purchase ;
import com.android.billingclient.api.Purchase ;
import com.android.billingclient.api.PurchasesUpdatedListener ;
import com.android.billingclient.api.PurchasesUpdatedListener ;
import com.android.billingclient.api.SkuDetails ;
import com.android.billingclient.api.SkuDetails ;
@ -56,7 +61,6 @@ import java.security.PublicKey;
import java.security.Signature ;
import java.security.Signature ;
import java.security.spec.X509EncodedKeySpec ;
import java.security.spec.X509EncodedKeySpec ;
import java.util.ArrayList ;
import java.util.ArrayList ;
import java.util.Arrays ;
import java.util.HashMap ;
import java.util.HashMap ;
import java.util.List ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
@ -69,15 +73,16 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
static final String ACTION_PURCHASE = BuildConfig . APPLICATION_ID + ".ACTION_PURCHASE" ;
static final String ACTION_PURCHASE = BuildConfig . APPLICATION_ID + ".ACTION_PURCHASE" ;
static final String ACTION_ACTIVATE_PRO = BuildConfig . APPLICATION_ID + ".ACTIVATE_PRO" ;
static final String ACTION_ACTIVATE_PRO = BuildConfig . APPLICATION_ID + ".ACTIVATE_PRO" ;
static final String SKU_PRO = BuildConfig . APPLICATION_ID + ".pro" ;
@Override
@Override
protected void onCreate ( Bundle savedInstanceState ) {
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState ) ;
super . onCreate ( savedInstanceState ) ;
if ( Helper . isPlayStoreInstall ( this ) ) {
if ( Helper . isPlayStoreInstall ( this ) ) {
Log . i ( "IAB start" ) ;
Log . i ( "IAB start" ) ;
billingClient = BillingClient . newBuilder ( this ) . setListener ( this ) . build ( ) ;
billingClient = BillingClient . newBuilder ( this )
. enablePendingPurchases ( )
. setListener ( this )
. build ( ) ;
billingClient . startConnection ( billingClientStateListener ) ;
billingClient . startConnection ( billingClientStateListener ) ;
}
}
}
}
@ -110,6 +115,13 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
super . onDestroy ( ) ;
super . onDestroy ( ) ;
}
}
static String getSkuPro ( ) {
if ( BuildConfig . DEBUG )
return "android.test.purchased" ;
else
return BuildConfig . APPLICATION_ID + ".pro" ;
}
protected Intent getIntentPro ( ) {
protected Intent getIntentPro ( ) {
if ( Helper . isPlayStoreInstall ( this ) )
if ( Helper . isPlayStoreInstall ( this ) )
return null ;
return null ;
@ -146,18 +158,15 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
private void onPurchase ( Intent intent ) {
private void onPurchase ( Intent intent ) {
if ( Helper . isPlayStoreInstall ( this ) ) {
if ( Helper . isPlayStoreInstall ( this ) ) {
BillingFlowParams . Builder flowParams = BillingFlowParams . newBuilder ( ) ;
BillingFlowParams . Builder flowParams = BillingFlowParams . newBuilder ( ) ;
if ( skuDetails . containsKey ( SKU_PRO ) ) {
if ( skuDetails . containsKey ( getSkuPro ( ) ) ) {
Log . i ( "IAB purchase SKU=" + skuDetails . get ( SKU_PRO ) ) ;
Log . i ( "IAB purchase SKU=" + skuDetails . get ( getSkuPro ( ) ) ) ;
flowParams . setSkuDetails ( skuDetails . get ( SKU_PRO ) ) ;
flowParams . setSkuDetails ( skuDetails . get ( getSkuPro ( ) ) ) ;
} else {
Log . i ( "IAB purchase SKU=" + SKU_PRO ) ;
flowParams . setSku ( SKU_PRO ) . setType ( BillingClient . SkuType . INAPP ) ;
}
}
int responseCode = billingClient . launchBillingFlow ( this , flowParams . build ( ) ) ;
BillingResult result = billingClient . launchBillingFlow ( this , flowParams . build ( ) ) ;
String text = getBillingResponseText ( res ponseCode ) ;
String text = getBillingResponseText ( res ult ) ;
Log . i ( "IAB launch billing flow response=" + text ) ;
Log . i ( "IAB launch billing flow response=" + text ) ;
if ( res ponseCode ! = BillingClient . BillingRespons e. OK )
if ( res ult. getRes ponseCode( ) ! = BillingClient . BillingRespons eCod e. OK )
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
} else
} else
Helper . view ( this , this , getIntentPro ( ) ) ;
Helper . view ( this , this , getIntentPro ( ) ) ;
@ -202,17 +211,16 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
private int backoff = 4 ; // seconds
private int backoff = 4 ; // seconds
@Override
@Override
public void onBillingSetupFinished ( @BillingClient.BillingResponse int responseCode ) {
public void onBillingSetupFinished ( BillingResult result ) {
String text = getBillingResponseText ( res ponseCode ) ;
String text = getBillingResponseText ( res ult ) ;
Log . i ( "IAB connected response=" + text ) ;
Log . i ( "IAB connected response=" + text ) ;
if ( ! getLifecycle ( ) . getCurrentState ( ) . isAtLeast ( Lifecycle . State . RESUMED ) )
if ( ! getLifecycle ( ) . getCurrentState ( ) . isAtLeast ( Lifecycle . State . RESUMED ) )
return ;
return ;
if ( res ponseCode = = BillingClient . BillingRespons e. OK ) {
if ( res ult. getRes ponseCode( ) = = BillingClient . BillingRespons eCod e. OK ) {
backoff = 4 ;
backoff = 4 ;
queryPurchases ( ) ;
queryPurchases ( ) ;
querySkuDetails ( ) ;
} else
} else
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
}
}
@ -232,14 +240,14 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
} ;
} ;
@Override
@Override
public void onPurchasesUpdated ( int responseCode , @Nullable List < Purchase > purchases ) {
public void onPurchasesUpdated ( BillingResult result , @Nullable List < Purchase > purchases ) {
String text = getBillingResponseText ( res ponseCode ) ;
String text = getBillingResponseText ( res ult ) ;
Log . i ( "IAB purchases updated response=" + text ) ;
Log . i ( "IAB purchases updated response=" + text ) ;
if ( ! getLifecycle ( ) . getCurrentState ( ) . isAtLeast ( Lifecycle . State . RESUMED ) )
if ( ! getLifecycle ( ) . getCurrentState ( ) . isAtLeast ( Lifecycle . State . RESUMED ) )
return ;
return ;
if ( res ponseCode = = BillingClient . BillingRespons e. OK )
if ( res ult. getRes ponseCode( ) = = BillingClient . BillingRespons eCod e. OK )
checkPurchases ( purchases ) ;
checkPurchases ( purchases ) ;
else
else
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
@ -247,48 +255,29 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
private void queryPurchases ( ) {
private void queryPurchases ( ) {
Purchase . PurchasesResult result = billingClient . queryPurchases ( BillingClient . SkuType . INAPP ) ;
Purchase . PurchasesResult result = billingClient . queryPurchases ( BillingClient . SkuType . INAPP ) ;
String text = getBillingResponseText ( result . get ResponseCode ( ) ) ;
String text = getBillingResponseText ( result . get BillingResult ( ) ) ;
Log . i ( "IAB query purchases response=" + text ) ;
Log . i ( "IAB query purchases response=" + text ) ;
if ( result . getResponseCode ( ) = = BillingClient . BillingResponse . OK )
if ( result . getResponseCode ( ) = = BillingClient . BillingResponse Code . OK )
checkPurchases ( result . getPurchasesList ( ) ) ;
checkPurchases ( result . getPurchasesList ( ) ) ;
else
else
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
Snackbar . make ( getVisibleView ( ) , text , Snackbar . LENGTH_LONG ) . show ( ) ;
}
}
private void querySkuDetails ( ) {
Log . i ( "IAB query SKUs" ) ;
SkuDetailsParams . Builder builder = SkuDetailsParams . newBuilder ( ) ;
builder . setSkusList ( Arrays . asList ( SKU_PRO ) ) ;
builder . setType ( BillingClient . SkuType . INAPP ) ;
billingClient . querySkuDetailsAsync ( builder . build ( ) ,
new SkuDetailsResponseListener ( ) {
@Override
public void onSkuDetailsResponse ( int responseCode , List < SkuDetails > skuDetailsList ) {
String text = getBillingResponseText ( responseCode ) ;
Log . i ( "IAB query SKUs response=" + text ) ;
if ( responseCode = = BillingClient . BillingResponse . OK ) {
for ( SkuDetails skuDetail : skuDetailsList ) {
Log . i ( "IAB SKU detail=" + skuDetail ) ;
skuDetails . put ( skuDetail . getSku ( ) , skuDetail ) ;
for ( IBillingListener listener : listeners )
listener . onSkuDetails ( skuDetail . getSku ( ) , skuDetail . getPrice ( ) ) ;
}
}
}
} ) ;
}
interface IBillingListener {
interface IBillingListener {
void onSkuDetails ( String sku , String price ) ;
void onSkuDetails ( String sku , String price ) ;
void onPurchasePending ( String sku ) ;
void onPurchased ( String sku ) ;
}
}
void addBillingListener ( final IBillingListener listener , LifecycleOwner owner ) {
void addBillingListener ( final IBillingListener listener , LifecycleOwner owner ) {
Log . i ( "Adding billing listener=" + listener ) ;
Log . i ( "Adding billing listener=" + listener ) ;
listeners . add ( listener ) ;
listeners . add ( listener ) ;
for ( SkuDetails skuDetail : skuDetails . values ( ) )
if ( billingClient ! = null & & billingClient . isReady ( ) )
listener. onSkuDetails ( skuDetail . getSku ( ) , skuDetail . getPrice ( ) ) ;
queryPurchases ( ) ;
owner . getLifecycle ( ) . addObserver ( new LifecycleObserver ( ) {
owner . getLifecycle ( ) . addObserver ( new LifecycleObserver ( ) {
@OnLifecycleEvent ( Lifecycle . Event . ON_DESTROY )
@OnLifecycleEvent ( Lifecycle . Event . ON_DESTROY )
@ -300,6 +289,9 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
}
}
private void checkPurchases ( List < Purchase > purchases ) {
private void checkPurchases ( List < Purchase > purchases ) {
List < String > query = new ArrayList < > ( ) ;
query . add ( getSkuPro ( ) ) ;
if ( purchases ! = null ) {
if ( purchases ! = null ) {
SharedPreferences prefs = PreferenceManager . getDefaultSharedPreferences ( this ) ;
SharedPreferences prefs = PreferenceManager . getDefaultSharedPreferences ( this ) ;
SharedPreferences . Editor editor = prefs . edit ( ) ;
SharedPreferences . Editor editor = prefs . edit ( ) ;
@ -308,7 +300,23 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
for ( Purchase purchase : purchases )
for ( Purchase purchase : purchases )
try {
try {
Log . i ( "IAB SKU=" + purchase . getSku ( ) ) ;
query . remove ( purchase . getSku ( ) ) ;
boolean purchased = ( purchase . getPurchaseState ( ) = = Purchase . PurchaseState . PURCHASED ) ;
Log . i ( "IAB SKU=" + purchase . getSku ( ) + " purchased=" + purchased ) ;
//if (new Date().getTime() - purchase.getPurchaseTime() > 3 * 60 * 1000L) {
// consumePurchase(purchase);
// continue;
//}
for ( IBillingListener listener : listeners )
if ( purchased )
listener . onPurchased ( purchase . getSku ( ) ) ;
else
listener . onPurchasePending ( purchase . getSku ( ) ) ;
if ( ! purchased )
continue ;
byte [ ] decodedKey = Base64 . decode ( getString ( R . string . public_key ) , Base64 . DEFAULT ) ;
byte [ ] decodedKey = Base64 . decode ( getString ( R . string . public_key ) , Base64 . DEFAULT ) ;
KeyFactory keyFactory = KeyFactory . getInstance ( "RSA" ) ;
KeyFactory keyFactory = KeyFactory . getInstance ( "RSA" ) ;
@ -317,10 +325,14 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
sig . initVerify ( publicKey ) ;
sig . initVerify ( publicKey ) ;
sig . update ( purchase . getOriginalJson ( ) . getBytes ( ) ) ;
sig . update ( purchase . getOriginalJson ( ) . getBytes ( ) ) ;
if ( sig . verify ( Base64 . decode ( purchase . getSignature ( ) , Base64 . DEFAULT ) ) ) {
if ( sig . verify ( Base64 . decode ( purchase . getSignature ( ) , Base64 . DEFAULT ) ) ) {
if ( SKU_PRO . equals ( purchase . getSku ( ) ) ) {
if ( getSkuPro ( ) . equals ( purchase . getSku ( ) ) ) {
editor . putBoolean ( "pro" , true ) ;
if ( purchase . isAcknowledged ( ) ) {
Log . i ( "IAB pro features activated" ) ;
editor . putBoolean ( "pro" , true ) ;
Log . i ( "IAB pro features activated" ) ;
} else
acknowledgePurchase ( purchase ) ;
}
}
} else {
} else {
Log . w ( "Invalid signature" ) ;
Log . w ( "Invalid signature" ) ;
Snackbar . make ( getVisibleView ( ) , R . string . title_pro_invalid , Snackbar . LENGTH_LONG ) . show ( ) ;
Snackbar . make ( getVisibleView ( ) , R . string . title_pro_invalid , Snackbar . LENGTH_LONG ) . show ( ) ;
@ -332,56 +344,120 @@ abstract class ActivityBilling extends ActivityBase implements PurchasesUpdatedL
editor . apply ( ) ;
editor . apply ( ) ;
}
}
if ( query . size ( ) > 0 )
querySkus ( query ) ;
}
private void querySkus ( List < String > query ) {
Log . i ( "IAB query SKUs" ) ;
SkuDetailsParams . Builder builder = SkuDetailsParams . newBuilder ( ) ;
builder . setSkusList ( query ) ;
builder . setType ( BillingClient . SkuType . INAPP ) ;
billingClient . querySkuDetailsAsync ( builder . build ( ) ,
new SkuDetailsResponseListener ( ) {
@Override
public void onSkuDetailsResponse ( BillingResult result , List < SkuDetails > skuDetailsList ) {
String text = getBillingResponseText ( result ) ;
Log . i ( "IAB query SKUs response=" + text ) ;
if ( result . getResponseCode ( ) = = BillingClient . BillingResponseCode . OK ) {
for ( SkuDetails skuDetail : skuDetailsList ) {
Log . i ( "IAB SKU detail=" + skuDetail ) ;
skuDetails . put ( skuDetail . getSku ( ) , skuDetail ) ;
for ( IBillingListener listener : listeners )
listener . onSkuDetails ( skuDetail . getSku ( ) , skuDetail . getPrice ( ) ) ;
}
}
}
} ) ;
}
private void consumePurchase ( final Purchase purchase ) {
Log . i ( "IAB SKU=" + purchase . getSku ( ) + " consuming" ) ;
ConsumeParams params = ConsumeParams . newBuilder ( )
. setPurchaseToken ( purchase . getPurchaseToken ( ) )
. build ( ) ;
billingClient . consumeAsync ( params , new ConsumeResponseListener ( ) {
@Override
public void onConsumeResponse ( BillingResult result , String purchaseToken ) {
String text = getBillingResponseText ( result ) ;
Log . i ( "IAB SKU=" + purchase . getSku ( ) + " consumed response=" + text ) ;
}
} ) ;
}
private void acknowledgePurchase ( final Purchase purchase ) {
Log . i ( "IAB acknowledging purchase SKU=" + purchase . getSku ( ) ) ;
AcknowledgePurchaseParams params =
AcknowledgePurchaseParams . newBuilder ( )
. setPurchaseToken ( purchase . getPurchaseToken ( ) )
. build ( ) ;
billingClient . acknowledgePurchase ( params , new AcknowledgePurchaseResponseListener ( ) {
@Override
public void onAcknowledgePurchaseResponse ( BillingResult result ) {
String text = getBillingResponseText ( result ) ;
Log . i ( "IAB acknowledged SKU=" + purchase . getSku ( ) + " response=" + text ) ;
if ( result . getResponseCode ( ) = = BillingClient . BillingResponseCode . OK ) {
SharedPreferences prefs = PreferenceManager . getDefaultSharedPreferences ( ActivityBilling . this ) ;
prefs . edit ( ) . putBoolean ( "pro" , true ) . apply ( ) ;
}
}
} ) ;
}
private static String getBillingResponseText ( BillingResult result ) {
String debug = result . getDebugMessage ( ) ;
return _getBillingResponseText ( result ) + ( debug = = null ? "" : " " + debug ) ;
}
}
static String getBillingResponseText ( @BillingClient.BillingResponse int responseCode ) {
private static String _getBillingResponseText ( BillingResult result ) {
switch ( responseCode ) {
switch ( res ult. getRes ponseCode( ) ) {
case BillingClient . BillingResponse . BILLING_UNAVAILABLE :
case BillingClient . BillingResponse Code . BILLING_UNAVAILABLE :
// Billing API version is not supported for the type requested
// Billing API version is not supported for the type requested
return "BILLING_UNAVAILABLE" ;
return "BILLING_UNAVAILABLE" ;
case BillingClient . BillingResponse . DEVELOPER_ERROR :
case BillingClient . BillingResponse Code . DEVELOPER_ERROR :
// Invalid arguments provided to the API.
// Invalid arguments provided to the API.
return "DEVELOPER_ERROR" ;
return "DEVELOPER_ERROR" ;
case BillingClient . BillingResponse . ERROR :
case BillingClient . BillingResponse Code . ERROR :
// Fatal error during the API action
// Fatal error during the API action
return "ERROR" ;
return "ERROR" ;
case BillingClient . BillingResponse . FEATURE_NOT_SUPPORTED :
case BillingClient . BillingResponse Code . FEATURE_NOT_SUPPORTED :
// Requested feature is not supported by Play Store on the current device.
// Requested feature is not supported by Play Store on the current device.
return "FEATURE_NOT_SUPPORTED" ;
return "FEATURE_NOT_SUPPORTED" ;
case BillingClient . BillingResponse . ITEM_ALREADY_OWNED :
case BillingClient . BillingResponse Code . ITEM_ALREADY_OWNED :
// Failure to purchase since item is already owned
// Failure to purchase since item is already owned
return "ITEM_ALREADY_OWNED" ;
return "ITEM_ALREADY_OWNED" ;
case BillingClient . BillingResponse . ITEM_NOT_OWNED :
case BillingClient . BillingResponse Code . ITEM_NOT_OWNED :
// Failure to consume since item is not owned
// Failure to consume since item is not owned
return "ITEM_NOT_OWNED" ;
return "ITEM_NOT_OWNED" ;
case BillingClient . BillingResponse . ITEM_UNAVAILABLE :
case BillingClient . BillingResponse Code . ITEM_UNAVAILABLE :
// Requested product is not available for purchase
// Requested product is not available for purchase
return "ITEM_UNAVAILABLE" ;
return "ITEM_UNAVAILABLE" ;
case BillingClient . BillingResponse . OK :
case BillingClient . BillingResponse Code . OK :
// Success
// Success
return "OK" ;
return "OK" ;
case BillingClient . BillingResponse . SERVICE_DISCONNECTED :
case BillingClient . BillingResponse Code . SERVICE_DISCONNECTED :
// Play Store service is not connected now - potentially transient state.
// Play Store service is not connected now - potentially transient state.
return "SERVICE_DISCONNECTED" ;
return "SERVICE_DISCONNECTED" ;
case BillingClient . BillingResponse . SERVICE_UNAVAILABLE :
case BillingClient . BillingResponse Code . SERVICE_UNAVAILABLE :
// Network connection is down
// Network connection is down
return "SERVICE_UNAVAILABLE" ;
return "SERVICE_UNAVAILABLE" ;
case BillingClient . BillingResponse . USER_CANCELED :
case BillingClient . BillingResponse Code . USER_CANCELED :
// User pressed back or canceled a dialog
// User pressed back or canceled a dialog
return "USER_CANCELED" ;
return "USER_CANCELED" ;
default :
default :
return Integer . toString ( responseCode ) ;
return Integer . toString ( res ult. getRes ponseCode( ) ) ;
}
}
}
}
}
}