Integrating In-app Subscriptions with Huawei. From Play Market to AppGallery.

AppGallery Connect admin screenshot

This how-to guide demonstrates my development journey of Huawei in-app subscription IAP integration that is based on the Huawei In-App Purchases kit.

This guide should be especially useful for those who perform the migration of their existing application from Play Market to Huawei AppGallery and is in need to integrate HMS IAP Services.

My Raloco Notes application relies on a subscription model to charge users on yearly basis for additional fonts and colors. As the subscription state can change any time or even be canceled I had to implement additional code scenarios to handle these logics.

It also turned out that Google and Huawei handle subscriptions differently.

Prerequisites

With all my desire to have one project for both marketplaces, I had to launch a separate development branch for Huawei, mainly due to numerous adjustments in project configurations, Gradle files, etc.

Here is my workflow and it can be separated into the following steps:

  1. Create new copy of Android Studio project.
  2. Configure project and Gradle scripts to work with Huawei SDK.
  3. Configure subscription and other dependencies under AppGallery Connect portal.
  4. Make first test compilation.
  5. Integrate new coding logics.
  6. Connect test user to check subscription in action.

1. Create new copy of Android Studio project

Two App Development Folders Screenshot

This step is very straightforward so I am skipping the description and moving directly to the next project configuration.

2. Configure project and Gradle scripts to work with Huawei SDK

As Huawei development environment is subject to frequent changes it is recommended to follow their original documentation. Start from Getting Started guide if your application is not yet published to Huawei AppGallery.

Alternatively, if your application is already successfully published under AppGallery start with the Integrating HMS Core SDK guide.

Below I am attaching my Gradle script files Play Market (Before version) and AppGallery (After version) to reflect changes in a more transparent way.

Play Market – gradle project before

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.2'
    }
}

allprojects {
    repositories {
        jcenter()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
    }
}

AppGallery – gradle project after

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        jcenter()
        maven {url 'https://developer.huawei.com/repo/'}
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.2'
        classpath 'com.huawei.agconnect:agcp:1.5.0.300'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven {url 'https://developer.huawei.com/repo/'}
    }
}

Play Market – gradle module before

apply plugin: 'com.android.application'

android {

    signingConfigs {
        signRelease {
            storeFile file('/Users/pavellukasenko/Documents/Apps/kenzap.keystore')
            storePassword 'xxxxx'
            keyAlias 'com.kenzap.notes'
            keyPassword 'xxxxx'
        }
    }

    compileSdkVersion 29
    buildToolsVersion "29.0.0"

    defaultConfig {
        applicationId "com.kenzap.notes"
        minSdkVersion 26
        targetSdkVersion 29
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
            signingConfig signingConfigs.signRelease
        }
        release_debug {
            debuggable true
            jniDebuggable true
            signingConfig signingConfigs.signRelease
            renderscriptDebuggable false
            minifyEnabled false
            zipAlignEnabled true
        }
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation "com.android.billingclient:billing:3.0.3"
    implementation 'androidx.multidex:multidex:2.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.browser:browser:1.3.0'
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
}

AppGallery – gradle module after

apply plugin: 'com.android.application'

android {

    signingConfigs {
        signRelease {
            storeFile file('/Users/pavellukasenko/Documents/Apps/kenzap.keystore')
            storePassword 'xxxxx'
            keyAlias 'com.kenzap.notes'
            keyPassword 'xxxxx'
        }
    }

    compileSdkVersion 29
    buildToolsVersion "29.0.0"

    defaultConfig {
        applicationId "com.kenzap.notes"
        minSdkVersion 26
        targetSdkVersion 29
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.signRelease
        }
        release_debug {
            debuggable true
            jniDebuggable true
            signingConfig signingConfigs.signRelease
            renderscriptDebuggable false
            minifyEnabled false
            zipAlignEnabled true
        }
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.huawei.hms:hwid:4.0.1.300'
    implementation 'com.huawei.hms:iap:4.0.0.300'

    implementation 'androidx.multidex:multidex:2.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.browser:browser:1.3.0'
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
}

apply plugin: 'com.huawei.agconnect'

3. Configure subscription and other dependencies under AppGallery Connect portal

AppGallery Connect. Creating IAB subscription. Product Management.

You need to register your subscription with the help of Huawei Developer console. Follow adding a product guide to achieve this step.

4. Make first test compilation

Before code integration, it is important to check if the project compiles. You will need to remove all old billing-related code dependencies as Google Billing SDK is no longer present.

Quite many things may go wrong at this stage so I am not providing any additional feedback here.

5. Integrate new coding logics

All my subscription and billing related activities are stored under single StoreActivity class. In a similar way as before I am providing two code samples Play Market before version and AppGallery after version.

Play Market StoreActivity class – before version

package com.kenzap.store;

/**
 * Created by Pavels Lukasenko on 05/12/21.
 */

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.kenzap.notes.C;
import com.kenzap.notes.HttpUtils;
import com.kenzap.notes.R;
import java.util.ArrayList;
import java.util.List;

public class StoreActivity extends Activity {

    private static final String TAG = "StoreActivity";

    Context c;
    SharedPreferences pref;
    IntentFilter filter = new IntentFilter();
    TextView centerTextList3;
    Button sub_btn;

    AlertDialog dialog = null;
    private boolean justPurchased = false;

    // billing variables
    private static BillingClient billingClient;
    private PurchasesUpdatedListener purchasesUpdatedListener;

    // status variables
    private boolean billingLoaded = false;
    private SkuDetails sku;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_store);
        getActionBar().hide();

        c = this;
        pref = PreferenceManager.getDefaultSharedPreferences(this);
        billingLoaded = false; // live billing status billing variable

        initStore();

        // REGISTER ASINHRONOUS BROADCAST RECEIVER - USED FOR HTTP REQUESTS
        filter.addAction("Async");

        // button binder
        sub_btn = (Button) findViewById(R.id.sub_btn);
        centerTextList3 = (TextView) findViewById(R.id.centerTextList3);

        // close listener
        ImageView c_btn = (ImageView) findViewById(R.id.closeStore);
        c_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                finish();
            }
        });
    }

    public static void verifySubscription(final Context context, final SharedPreferences pref) {

        if (!HttpUtils.isNetAvailable(context))
            return;

        C.log("verifySubscription");
        PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
            @Override
            public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {

            }
        };

        billingClient = BillingClient.newBuilder(context)
                .setListener(purchasesUpdatedListener)
                .enablePendingPurchases()
                .build();

        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {

                    // The BillingClient is ready - query purchases.
                    C.log("Get active subscriptions");
                    // {"orderId":"GPA.3342-0466-6668-34182","packageName":"com.kenzap.notes","productId":"notes_subscription","purchaseTime":1615557934948,"purchaseState":0,"purchaseToken":"lphlcipkjaheaallkkglbcdo.AO-J1Ow6-pC_pTLm2qzFvaeOoiYDtvla46_HVvzoj_f6bhvElncpuwZA6oGut9mFpLl7JokCdbMMDg4B0AZJffrsykPoRhj2mw","autoRenewing":true,"acknowledged":false}
                    Purchase.PurchasesResult pr = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
                    List<Purchase> pList = pr.getPurchasesList();

                    // something totally wrong
                    if(pList==null) return;

                    // no active subscriptions
                    if(pList.size() == 0){
                        C.log("No active subscriptions");
                        StoreActivity.subscription(false, pref);
                    }

                    for (Purchase purchase : pList) {

                        // purchased but not acknowledged do now instead
                        if(purchase.getSku().equals(C.products[0])) {

                            // make sure subscription is operational
                            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                                if (!purchase.isAcknowledged()) {
                                    AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                                            .setPurchaseToken(purchase.getPurchaseToken())
                                            .build();
                                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
                                        @Override
                                        public void onAcknowledgePurchaseResponse(BillingResult billingResult) {

                                            StoreActivity.subscription(true, pref);

                                        }
                                    });
                                }

                            // cancel subscription
                            }else{

                                StoreActivity.subscription(false, pref);
                            }
                        }

                        // TODO store subscription data on the backend for marketing purposes
                        C.log(purchase.getOriginalJson());

                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
            }
        });

        billingClient.queryPurchases(BillingClient.SkuType.SUBS);
    }

    // trigger subscription status
    public static void subscription(Boolean status, SharedPreferences pref){

        // enable subscription
        if(status){

            C.log("Enable subscription");
            SharedPreferences.Editor editor = pref.edit();
            editor.putBoolean("upgrade1", true);
            editor.putLong("upgrade1_time", (System.currentTimeMillis() / 1000));
            editor.commit();

        // disable subscription
        }else{

            C.log("Cancel subscription");
            SharedPreferences.Editor editor = pref.edit();
            editor.putBoolean("upgrade1", false);
            editor.putLong("upgrade1_time", (System.currentTimeMillis() / 1000));
            editor.commit();
        }
    }

    public void initStore(){

        purchasesUpdatedListener = new PurchasesUpdatedListener() {
            @Override
            public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {

                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {

                    for (Purchase purchase : purchases) {

                        // acknowledge purchase. Google requirement
                        handlePurchase(purchase);
                    }
                } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {


                } else {

                    String msg = "";
                    switch (billingResult.getResponseCode()){

                        case  BillingClient.BillingResponseCode.BILLING_UNAVAILABLE: msg = "Billing API version is not supported for the type requested"; break;
                        case  BillingClient.BillingResponseCode.DEVELOPER_ERROR: msg = "Invalid arguments provided to the API. This error can also indicate that the application was not correctly signed or properly set up for In-app Billing in Google Play, or does not have the necessary permissions in its manifest."; break;
                        case  BillingClient.BillingResponseCode.ERROR: msg = "Fatal error during the API action."; break;
                        case  BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED: msg = "Requested feature is not supported by Play Store on the current device."; break;
                        case  BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED: msg = "Failure to purchase since item is already owned."; finishUpgrade(); break;
                        case  BillingClient.BillingResponseCode.ITEM_NOT_OWNED: msg = "Failure to consume since item is not owned."; break;
                        case  BillingClient.BillingResponseCode.ITEM_UNAVAILABLE: msg = "Requested product is not available for purchase."; break;
                        case  BillingClient.BillingResponseCode.SERVICE_DISCONNECTED: msg = "Requested product is not available for purchase."; billingLoaded = false; break;
                        case  BillingClient.BillingResponseCode.SERVICE_TIMEOUT: msg = getResources().getString(R.string.i1); break;
                        case  BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE: msg = getResources().getString(R.string.i1); break;
                    }

                    // notify user
                    if(!msg.isEmpty()) Toast.makeText(c, msg, Toast.LENGTH_SHORT).show();

                }
            }
        };

        billingClient = BillingClient.newBuilder(getApplicationContext())
                .setListener(purchasesUpdatedListener)
                .enablePendingPurchases()
                .build();

        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {

                billingLoaded = true;
                if (billingResult.getResponseCode() ==  BillingClient.BillingResponseCode.OK) {

                    // The BillingClient is ready. Lets get available purchases
                    getProducts();
                }
            }
            @Override
            public void onBillingServiceDisconnected() {

                billingLoaded = false;
            }
        });
    }

    // consume the purchase otherwise Google will refund it.
    void handlePurchase(Purchase purchase) {

        C.log("handlePurchase: "+purchase.getOriginalJson());

        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.getPurchaseToken())
                        .build();
                billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
                    @Override
                    public void onAcknowledgePurchaseResponse(BillingResult billingResult) {

                        finishUpgrade();
                    }
                });
            }
        }
    }

    public void getProducts() {

        C.log("StoreActivity getProducts");

        List<String> skuList = new ArrayList<>();
        skuList.add(C.products[0]);
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
        billingClient.querySkuDetailsAsync(params.build(),
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {

                        C.log("StoreActivity " + billingResult.getResponseCode());
                        if (skuDetailsList == null) {
                            Toast.makeText(c, getResources().getString(R.string.loading), Toast.LENGTH_SHORT).show();
                            return;
                        }
                        if (skuDetailsList.size() == 0) {
                            Toast.makeText(c, getResources().getString(R.string.loading), Toast.LENGTH_SHORT).show();
                            return;
                        }

                        sku = skuDetailsList.get(0);
                        centerTextList3.setText(sku.getPrice() + "/" + getResources().getString(R.string.store8));

                    }
                });
    }

    @Override
    public void onResume(){
        super.onResume();

        // if by nay chanche user opens store while already subscribed close it immediately
        if(pref.getBoolean("upgrade1",false)) finishUpgrade();

        // prevent store from opening when internet is down
        if(!HttpUtils.isNetAvailable(c)) {

            Toast.makeText(c, getResources().getString(R.string.i1), Toast.LENGTH_LONG).show();
            finish();
        }
    }

    @Override
    protected void onPause() {

        super.onPause();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    private void subscribe(){

        // current time
        long now = System.currentTimeMillis()/1000;

        // prevent double clicks 5s safe interval
        if(now - pref.getLong("last_subscribe_time", 0) < 5) { return; }
        pref.edit().putLong("last_subscribe_time", now).apply();

        // already subscribed
        if(pref.getBoolean("upgrade1", false)){ Toast.makeText(c, getResources().getString(R.string.upgrade_success), Toast.LENGTH_LONG).show(); return; }

        // check internet connection
        if(!HttpUtils.isNetAvailable(c)) {  Toast.makeText(c, getResources().getString(R.string.i1), Toast.LENGTH_LONG).show(); return; }

        // billing not loaded
        if(!billingLoaded || billingClient==null){ Toast.makeText(c, getResources().getString(R.string.i2), Toast.LENGTH_LONG).show(); return; }

        C.log("subscribing"+sku.getDescription());
        BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(sku)
                .build();

        int responseCode = billingClient.launchBillingFlow(this, billingFlowParams).getResponseCode();

        C.log(""+responseCode);
    }

    // button frontend listener
    public void upgrade1(View v){

        // do subscription
        subscribe();
    }

    // notify of successful upgrade, cache subscription as enabled and close store activity
    private void finishUpgrade(){

        Toast.makeText(c, getResources().getString(R.string.upgrade_success), Toast.LENGTH_LONG).show();
        StoreActivity.subscription(true, pref);
        finish();
    }
}

AppGallery StoreActivity class – after version

package com.kenzap.store;

/**
 * Created by Pavels Lukasenko on 05/12/21.
 */

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.huawei.hmf.tasks.OnFailureListener;
import com.huawei.hmf.tasks.OnSuccessListener;
import com.huawei.hmf.tasks.Task;
import com.huawei.hms.iap.Iap;
import com.huawei.hms.iap.IapApiException;
import com.huawei.hms.iap.IapClient;
import com.huawei.hms.iap.entity.InAppPurchaseData;
import com.huawei.hms.iap.entity.IsEnvReadyResult;
import com.huawei.hms.iap.entity.OrderStatusCode;
import com.huawei.hms.iap.entity.OwnedPurchasesReq;
import com.huawei.hms.iap.entity.OwnedPurchasesResult;
import com.huawei.hms.iap.entity.ProductInfo;
import com.huawei.hms.iap.entity.ProductInfoReq;
import com.huawei.hms.iap.entity.ProductInfoResult;
import com.huawei.hms.iap.entity.PurchaseIntentReq;
import com.huawei.hms.iap.entity.PurchaseIntentResult;
import com.huawei.hms.iap.entity.PurchaseResultInfo;
import com.huawei.hms.support.api.client.Status;

import com.kenzap.notes.C;
import com.kenzap.notes.HttpUtils;
import com.kenzap.notes.R;

import org.json.JSONException;

import java.util.ArrayList;
import java.util.List;

public class StoreActivity extends Activity {

    private static final String TAG = "StoreActivity";

    Context c;
    SharedPreferences pref;
    IntentFilter filter = new IntentFilter();
    TextView centerTextList3;
    Button sub_btn;

    private boolean billingLoaded = false;

    // obtain this under AppGallery Connect > Project Settings > Earn > In-app Purchases > Configuration 
    private static final String publicKey = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAl4cOrmx6iCIGfALLH74NK8f+crISGcZ5qsOz05qfC1iQuxnWVrdrigOeLtN11TXZDbp9H+kBx94J1kmB5Np3WXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=";
    public static final Integer REQ_CODE_BUY_CONSUMABLE = 4002;
    public static final Integer REQ_CODE_BUY_NON_CONSUMABLE = 4003;
    public static final Integer REQ_CODE_BUY_SUPSCRIPTION = 4004;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_store);
        getActionBar().hide();

        c = this;
        pref = PreferenceManager.getDefaultSharedPreferences(this);
        billingLoaded = false; // live billing status billing variable

        initStore();

        // REGISTER ASINHRONOUS BROADCAST RECEIVER - USED FOR HTTP REQUESTS
        filter.addAction("Async");

        // button binder
        sub_btn = (Button) findViewById(R.id.sub_btn);
        centerTextList3 = (TextView) findViewById(R.id.centerTextList3);

        // close listener
        ImageView c_btn = (ImageView) findViewById(R.id.closeStore);
        c_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                finish();
            }
        });
    }

    /*
    This method is used from apps main activity to verify current subscription state without entering this store activity
    User may choose to cancel or pause subscription any time thus additional verification is needed.

    Quering Huawei of all active subscriptions the raw response of each separate subscription may look as following:
    {"autoRenewing":false,"subIsvalid":true,"orderId":"1615581759140.301DBC76.5715","lastOrderId":"L1615581759140.301DBC76.5715","packageName":"com.kenzap.notes","applicationId":102479425,"productId":"notes_subscription","kind":2,"productName":"Fonts & Colors","productGroup":"C8749D5AEE6640B794598FAF1CB765B0","purchaseTime":1615581934071,"oriPurchaseTime":1615581934071,"purchaseState":1,"purchaseToken":"00000178282d46a4abaa006cfb6578abb3552f0d0c353e323d6be1ee947d7b22156218eda4ea3c66x5347.5.5715","purchaseType":0,"currency":"SGD","price":315,"country":"SG","subscriptionId":"1615581759140.3F419BAC.5715","quantity":1,"daysLasted":0,"numOfPeriods":1,"numOfDiscount":0,"expirationDate":1615585534071,"retryFlag":1,"introductoryFlag":0,"trialFlag":0,"renewStatus":0,"renewPrice":315,"cancelledSubKeepDays":30,"payOrderId":"SandBox_1615581759140.301DBC76.5715","payType":"0","confirmed"

    If response is successful but nothing is returned assuming that subscription does not exists any long and we cancel additional functionality
    */
    public static void verifySubscription(final Context context, final SharedPreferences pref) {

        if (!HttpUtils.isNetAvailable(context))
            return;

        C.log("verifySubscription");

        // Constructs a OwnedPurchasesReq object.
        OwnedPurchasesReq req = new OwnedPurchasesReq();
        // 0: consumable; 1: non-consumable; 2: auto-renewable subscription
        req.setPriceType(IapClient.PriceType.IN_APP_SUBSCRIPTION);
        // to call the obtainOwnedPurchaseRecord API.
        // To get the Activity instance that calls this API.
        // Activity activity = context;
        Task<OwnedPurchasesResult> task = Iap.getIapClient(context).obtainOwnedPurchaseRecord(req);
        task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
            @Override
            public void onSuccess(OwnedPurchasesResult result) {

                // no active subscriptions cancel current if not yet
                if(result.getInAppPurchaseDataList().size()==0)  StoreActivity.subscription(false, pref);

                for (int i = 0; i < result.getInAppPurchaseDataList().size(); i++) {
                    String inAppPurchaseData = result.getInAppPurchaseDataList().get(i);
                    String InAppSignature = result.getInAppSignature().get(i);

                    C.log("inAppPurchaseData"+inAppPurchaseData);
                    // use the payment public key to verify the signature of the inAppPurchaseData.
                    // if success.
                    try {
                        InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
                        int purchaseState = inAppPurchaseDataBean.getPurchaseState();

                        // find subscription by its ID
                        C.log(inAppPurchaseDataBean.getProductId());
                        if(inAppPurchaseDataBean.getProductId().equals(C.products[0])) {

                            // subscription is valid
                            if (inAppPurchaseDataBean.isSubValid()) {
                                StoreActivity.subscription(true, pref);

                                // cancel loop on first successful subscription found as response may contain multiple records with canceled subscriptions too.
                                return;
                            } else {
                                StoreActivity.subscription(false, pref);
                            }
                        }

                    } catch (JSONException e) {
                        C.log("error:"+e);
                    }
                }
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(Exception e) {
                if (e instanceof IapApiException) {
                    IapApiException apiException = (IapApiException)e;
                    int returnCode = apiException.getStatusCode();
                } else {
                    // Other external errors
                }
            }
        });
    }

    // trigger subscription status
    public static void subscription(Boolean status, SharedPreferences pref){

        // enable subscription
        if(status){

            C.log("Enable subscription");
            SharedPreferences.Editor editor = pref.edit();
            editor.putBoolean("upgrade1", true);
            editor.putLong("upgrade1_time", (System.currentTimeMillis() / 1000));
            editor.commit();

        // disable subscription
        }else{

            C.log("Cancel subscription");
            SharedPreferences.Editor editor = pref.edit();
            editor.putBoolean("upgrade1", false);
            editor.putLong("upgrade1_time", (System.currentTimeMillis() / 1000));
            editor.commit();
        }
    }

    // retrieve list of availbale products to purchase. Notes subscription in our case
    public void initStore(){

        C.log("initStore");

        ProductInfoReq productInfoReq = new ProductInfoReq();
        productInfoReq.setPriceType(IapClient.PriceType.IN_APP_SUBSCRIPTION);
        List<String> skuList = new ArrayList<>();
        skuList.add(C.products[0]);
        productInfoReq.setProductIds(skuList);

        Task<ProductInfoResult> task = Iap.getIapClient(this).obtainProductInfo(productInfoReq);
        task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
            @Override
            public void onSuccess(ProductInfoResult result) {
                // Obtain the result
                C.log("initStore result: "+result);
                List<ProductInfo> productList = result.getProductInfoList();
                for(ProductInfo sku : productList){

                    C.log("Product: "+sku.getProductId());
                    if(sku.getProductId().equals(C.products[0])) {
                        centerTextList3.setText(sku.getPrice() + "/" + getResources().getString(R.string.store8));
                    }

                    C.log("Price: "+sku.getPrice());
                }
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(Exception e) {

                // C.log("Error: "+e);
                if (e instanceof IapApiException) {
                    IapApiException apiException = (IapApiException)e;
                    int returnCode = apiException.getStatusCode();
                    Toast.makeText(c, apiException.getMessage(), Toast.LENGTH_SHORT).show();


                    C.log("Error: "+e);

                } else {
                    // Other external errors
                }
            }
        });
    }

    private void deliverProduct(final String inAppPurchaseData, final String inAppPurchaseDataSignature) {

        C.log("deliverProduct");
        final String  TAG = "deliverProduct";
        if (TextUtils.isEmpty(inAppPurchaseData) || TextUtils.isEmpty(inAppPurchaseDataSignature)) {
            return;
        }

        if (!SecurityUtil.doCheck(inAppPurchaseData, inAppPurchaseDataSignature, publicKey)) {

            C.log("verify_signature_fail");
            Toast.makeText(c, "Can not verify purchase signature.", Toast.LENGTH_SHORT).show();
            return;
        }

        finishUpgrade();

    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        final String TAG = "onActivityResult";
        super.onActivityResult(requestCode, resultCode, data);

        if (data == null){
            return;
        }

        C.log(requestCode + " onActivityResultr:"+resultCode);

        PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data);
        if (purchaseResultInfo == null) {
            C.log("purchaseResultInfo can't be null");
            return;
        }

        // iterate through response codes
        switch(purchaseResultInfo.getReturnCode()){

            case OrderStatusCode.ORDER_STATE_NET_ERROR: Toast.makeText(c, "Network connection error.", Toast.LENGTH_SHORT).show(); break;
            case OrderStatusCode.ORDER_HWID_NOT_LOGIN: Toast.makeText(c, "The HUAWEI ID is not signed in.", Toast.LENGTH_SHORT).show(); break;
            case OrderStatusCode.ORDER_PRODUCT_OWNED: finishUpgrade(); return;
            case OrderStatusCode.ORDER_PRODUCT_CONSUMED: Toast.makeText(c, "The product has been consumed and cannot be consumed again.", Toast.LENGTH_SHORT).show(); break;
            case OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED: Toast.makeText(c, "The country or region of the signed-in HUAWEI ID does not support HUAWEI  IAP.", Toast.LENGTH_SHORT).show(); break;
            case 60056: Toast.makeText(c, "Can not continue. Transaction is not secure.", Toast.LENGTH_SHORT).show(); break;
        }

        if(requestCode == REQ_CODE_BUY_SUPSCRIPTION){
            if(purchaseResultInfo.getReturnCode() == OrderStatusCode.ORDER_STATE_SUCCESS) {
                String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
                String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
                deliverProduct(inAppPurchaseData, inAppPurchaseDataSignature);
                return;
            }
        }
    }

    @Override
    public void onResume(){
        super.onResume();

        // if by nay chanche user opens store while already subscribed close it immediately
        if(pref.getBoolean("upgrade1",false)) finishUpgrade();

        // prevent store from opening when internet is down
        if(!HttpUtils.isNetAvailable(c)) {

            Toast.makeText(c, getResources().getString(R.string.i1), Toast.LENGTH_LONG).show();
            finish();
        }
    }

    @Override
    protected void onPause() {

        super.onPause();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    // check if user is signed in to huawei etc
    private void isEnvReadyResult(){
        Task<IsEnvReadyResult> task = Iap.getIapClient(StoreActivity.this).isEnvReady();
        task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
            @Override
            public void onSuccess(IsEnvReadyResult result) { }

        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(Exception e) {
                if (e instanceof IapApiException) {
                    IapApiException apiException = (IapApiException) e;
                    Status status = apiException.getStatus();
                    if (status.getStatusCode() == OrderStatusCode.ORDER_HWID_NOT_LOGIN) {
                        if (status.hasResolution()) {
                            try {
                                status.startResolutionForResult(StoreActivity.this, 4444);
                            } catch (IntentSender.SendIntentException ex) {
                                ex.printStackTrace();
                            }
                        }

                    } else if (status.getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
                        Toast.makeText(getApplicationContext(),"This is unavailable in your country/region.", Toast.LENGTH_LONG).show();
                    }
                }
            }
        });
    }

    private void subscribe(){

        // current time
        long now = System.currentTimeMillis()/1000;

        // prevent double clicks 5s safe interval
        if(now - pref.getLong("last_subscribe_time", 0) < 5) { return; }
        pref.edit().putLong("last_subscribe_time", now).apply();

        // already subscribed
        if(pref.getBoolean("upgrade1", false)){ Toast.makeText(c, getResources().getString(R.string.upgrade_success), Toast.LENGTH_LONG).show(); return; }

        // check internet connection
        if(!HttpUtils.isNetAvailable(c)) {  Toast.makeText(c, getResources().getString(R.string.i1), Toast.LENGTH_LONG).show(); return; }


        final String tag = "purchaseStar";
        isEnvReadyResult();
        PurchaseIntentReq request =  new PurchaseIntentReq();
        request.setPriceType(IapClient.PriceType.IN_APP_SUBSCRIPTION);
        request.setProductId("notes_subscription");

        Task<PurchaseIntentResult> task = Iap.getIapClient(StoreActivity.this).createPurchaseIntent(request);
        task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
            @Override
            public void onSuccess(PurchaseIntentResult result) {

                Status status = result.getStatus();
                C.log("status: "+status);

                try {
                    if (status.hasResolution()) {
                        status.startResolutionForResult(StoreActivity.this, REQ_CODE_BUY_SUPSCRIPTION);
                    }
                } catch (IntentSender.SendIntentException e) {
                    Log.e(tag, e.getMessage());
                }
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(Exception e) {
                if (e instanceof IapApiException) {
                    IapApiException apiException = (IapApiException) e;
                    Status status = apiException.getStatus();
                    int returnCode = apiException.getStatusCode();
                    Log.e(tag, "status" + status + "error code " + returnCode);
                }
                Log.e(tag, "onFailure: " + e.getMessage());
            }
        });
    }

    // button frontend listener
    public void upgrade1(View v){

        // do subscription
        subscribe();
    }

    // notify of successful upgrade, cache subscription as enabled and close store activity
    private void finishUpgrade(){

        Toast.makeText(c, getResources().getString(R.string.upgrade_success), Toast.LENGTH_LONG).show();
        StoreActivity.subscription(true, pref);
        finish();
    }
}

Note that simply copy pasting these code samples will raise errors because some dependencies are missing such as internal logging method C.log or localized string declarations.

However you can easily tweak this code to adjust it according to your needs as it a production working solution.

One method from the code snippets above is declared as static, example: public static void verifySubscription.. It runs independently from other methods in this class.

This is done on purpose and allows other activities to query subscription state. The verifySubscription method is called from app’s main activity every time it is created. This ensures that user gets the latest subscription state should it was paused, resumed or canceled outside of the application.

Store activity visual part

Visual store activity representation in Android Studio

The image above demonstrates visual representation of Raloco Notes store activity. When user opens this activity the code initiates a call to the Play Market or Huawei SDK respectively to query subscription prices and period as specified in the portal.

The code below is the XML version of the store activity.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:src="@mipmap/ic_launcher"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/screen_wait"
        android:layout_gravity="center"
        android:contentDescription="@string/app_name"
        android:visibility="gone" />

    <ScrollView
        android:id="@+id/main_cont"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout

            android:id="@+id/screen_main"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:alpha="0.95"
            android:gravity="top"
            android:orientation="vertical"
            android:paddingLeft="20dp"
            android:paddingRight="20dp">

            <ImageView
                android:id="@+id/closeStore"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_gravity="right"
                android:layout_marginTop="12dp"
                android:layout_weight="2"
                android:src="@drawable/ic_close" />

            <TextView
                android:id="@+id/centerText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="0dp"
                android:layout_marginBottom="25dp"
                android:layout_weight="1"
                android:gravity="center"
                android:keepScreenOn="true"
                android:text="@string/store7"
                android:textAlignment="center"
                android:textColor="@color/text_blue"
                android:textSize="24sp"
                android:textStyle="bold" />

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="match_parent"
                android:layout_height="120dp"
                android:layout_gravity="center"
                android:layout_marginTop="0dp"
                android:layout_weight="1"
                android:src="@drawable/logo" />

            <TextView
                android:id="@+id/centerTextList1"
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center"
                android:keepScreenOn="true"
                android:text="@string/store1"
                android:textColor="@color/gray"
                android:textSize="24sp"
                android:layout_marginTop="20dp"
                android:layout_marginBottom="10dp"
                android:textStyle="italic" />

            <TextView
                android:id="@+id/centerTextList2"
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center"
                android:keepScreenOn="true"
                android:text="@string/store2"
                android:textColor="@color/gray"
                android:textSize="24sp"
                android:layout_marginTop="10dp"
                android:layout_marginBottom="15dp"
                android:textStyle="italic" />

            <TextView
                android:id="@+id/centerTextList3"
                android:layout_weight="1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center"
                android:keepScreenOn="true"
                android:text=""
                android:textColor="@color/holo_blue"
                android:textSize="18sp"
                android:layout_marginTop="10dp"
                android:layout_marginBottom="5dp"
                android:textStyle="bold" />


            <!--<TextView-->
                <!--android:id="@+id/centerTextList3"-->
                <!--android:layout_weight="1"-->
                <!--android:layout_width="wrap_content"-->
                <!--android:layout_height="wrap_content"-->
                <!--android:layout_gravity="center"-->
                <!--android:gravity="center"-->
                <!--android:keepScreenOn="true"-->
                <!--android:text="@string/store3"-->
                <!--android:textColor="@color/gray"-->
                <!--android:textSize="18sp"-->
                <!--android:layout_marginTop="15dp"-->
                <!--android:layout_marginBottom="25dp"-->
                <!--android:textStyle="italic" />-->

            <!--<TextView-->
                <!--android:id="@+id/centerTextList4"-->
                <!--android:layout_weight="1"-->
                <!--android:layout_width="wrap_content"-->
                <!--android:layout_height="wrap_content"-->
                <!--android:layout_gravity="center"-->
                <!--android:gravity="center"-->
                <!--android:keepScreenOn="true"-->
                <!--android:text="@string/store4"-->
                <!--android:textColor="@color/gray"-->
                <!--android:textSize="18sp"-->
                <!--android:layout_marginTop="15dp"-->
                <!--android:layout_marginBottom="25dp"-->
                <!--android:textStyle="italic" />-->

            <ImageView
                android:id="@+id/store_textb3"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:contentDescription="@string/store5"
                android:scaleType="centerCrop" />
        </LinearLayout>

    </ScrollView>

    <TextView
        android:id="@+id/noteText"
        android:gravity="left"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:keepScreenOn="true"
        android:text="@string/store5"
        android:textColor="@color/gray"
        android:textSize="12sp"
        android:layout_marginTop="15dp"
        android:layout_marginLeft="5dp"
        android:layout_marginBottom="70dp"
        android:textStyle="normal" />


    <Button
        android:id="@+id/sub_btn"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="@color/holo_blue"
        android:layout_gravity="bottom"
        android:text="SUBSCRIBE"
        android:textSize="20sp"
        android:textStyle="bold"
        android:padding="16dp"
        android:layout_margin="2dp"
        android:layout_marginBottom="1dp"
        android:textColor="@color/white"
        android:onClick="upgrade1"
        />
</FrameLayout>

6. Connect test user to check subscription in action

Before moving to production subscription workflow has to be properly tested. AppGallery supports test user registration.

Every connected test user should have a valid Huawei ID account. It took me around 15 minutes before my old Huawei ID account transformed into a test user account. During the first test transaction, it still asks for valid card details, however, under the test mode, the card is not being charged.

Follow the Managing Tester Accounts guide to add a test user to your application.

Some parts of the code were inspired by this How to Use HMS In-App Purchase tutorial.

Thank you for getting through this article, hope it was useful for you. Just in case you want to reach me directly please refer to this page.