Thanks @Webfreak, your answer for Kotlin guided to me to the right direction.
Here is how I implemented it for Java:
First add the 'billingclient' library to gradle :
implementation 'com.android.billingclient:billing:X.X.X'
And add the required permissions in the manifest file:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
The Activity must implements the following interfaces:
public class MainActivity extends AppCompatActivity implements
...
PurchasesUpdatedListener,
AcknowledgePurchaseResponseListener {
Then I initialize the billing client inside the onCreate method:
/** IN-APPS PURCHASE */
private BillingClient mBillingClient;
private long mLastPurchaseClickTime = 0;
private List<String> mSkuList = new ArrayList<>();
private List<SkuDetails> mSkuDetailsList = new ArrayList<>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// AppPrefs is just a standalone class I used to get or set shared preferences easily
mPrefs = AppPrefs.getInstance(this);
// Rest of your code ...
/** IN-APP PURCHASES */
// Initialize the list of all the in-app product IDs I use for this app
mSkuList.add(Parameters.UNIT_P1);// NoAdsPurchased
mSkuList.add(Parameters.UNIT_P2);// CustomizationPurchased
mSkuList.add(Parameters.UNIT_P3);// ChartsPurchased
// Initialize the billing client
setupBillingClient();
// Apply the upgrades on my app according to the user's purchases
applyUpgrades();
}
The method setting up the billing client is here, along with the metyhod I use to retrieve the avaialble in-app products fro the app:
private void setupBillingClient() {
mBillingClient = BillingClient
.newBuilder(MainActivity.this)
.enablePendingPurchases() // Useful for physical stores
.setListener(MainActivity.this)
.build();
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Load the available products related to the app from Google Play
getAvailableProducts();
Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);// Or SkuType.SUBS if subscriptions
// Init all the purchases to false in the shared preferences (security prevention)
mPrefs.setNoAdsPurchased(false);
mPrefs.setCustomizationPurchased(false);
mPrefs.setChartsPurchased(false);
// Retrieve and loop all the purchases done by the user
// Update all the boolean related to the purchases done in the shared preferences
if (purchasesResult.getPurchasesList() != null) {
for (Purchase purchase : purchasesResult.getPurchasesList()) {
if (purchase.isAcknowledged()) {
Log.e(TAG, purchase.getSku());
switch (purchase.getSku()) {
case Parameters.UNIT_P1:
mPrefs.setNoAdsPurchased(true);
break;
case Parameters.UNIT_P2:
mPrefs.setCustomizationPurchased(true);
break;
case Parameters.UNIT_P3:
mPrefs.setChartsPurchased(true);
break;
}
}
}
}
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
// TODO Note: It's strongly recommended that you implement your own connection retry policy and override the onBillingServiceDisconnected() method. Make sure you maintain the BillingClient connection when executing any methods.
Log.e(TAG, "onBillingServiceDisconnected");
}
});
}
private void getAvailableProducts() {
if (mBillingClient.isReady()) {
SkuDetailsParams params = SkuDetailsParams
.newBuilder()
.setSkusList(mSkuList)
.setType(BillingClient.SkuType.INAPP)
.build();
mBillingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
mSkuDetailsList = skuDetailsList;
}
}
});
}
}
When a purchase is done by the user (I allow purchases on several Fragments in my app), I call this function on the main Activity (using an interface):
@Override
public void purchase(String sku) {
// Mis-clicking prevention, using threshold of 3 seconds
if (SystemClock.elapsedRealtime() - mLastPurchaseClickTime < 3000){
Log.d(TAG, "Purchase click cancelled");
return;
}
mLastPurchaseClickTime = SystemClock.elapsedRealtime();
// Retrieve the SKU details
for (SkuDetails skuDetails : mSkuDetailsList) {
// Find the right SKU
if (sku.equals(skuDetails.getSku())) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
mBillingClient.launchBillingFlow(MainActivity.this, flowParams);
break;
}
}
}
Here I implement the methods inherited:
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else {
displayError(R.string.inapp_purchase_problem, billingResult.getResponseCode());
}
}
private void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
// Grant entitlement to the user.
applyPurchase(purchase);
// Acknowledge the purchase if it hasn't already been acknowledged.
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, MainActivity.this);
}
}
}
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
displayError(R.string.inapp_purchase_success, billingResult.getResponseCode());
}
}
The method I added to acknowledge a purchase on my app:
private void applyPurchase(Purchase purchase) {
switch (purchase.getSku()) {
case Parameters.UNIT_P1:
mPrefs.setNoAdsPurchased(true);
break;
case Parameters.UNIT_P2:
mPrefs.setCustomizationPurchased(true);
break;
case Parameters.UNIT_P3:
mPrefs.setChartsPurchased(true);
break;
}
// I remove the ads right away if purchases
if(mPrefs.getNoAdsPurchased()) {
destroyAds();
}
}
This last method is used to apply all the upgrades/purchases on the app (with an example with the removal of the ads):
private void applyUpgrades() {
// No ads
if (mPrefs.getNoAdsPurchased()) {
destroyAds();
} else {
loadAds();
}
if (mPrefs.getCustomizationPurchased()) {
// Allow customization
// ...
}
if (mPrefs.getChartsPurchased()) {
// Allow charts visualization
// ...
}
}
I guess this solution is not perfect yet but it is working, I will modify the code if I find improvements.