In-app billing acknowledgePurchase issue [using google play in-app billing library] - java

I am new to android and trying to implement in-app billing first time.
I am using google play in-app library.
https://developer.android.com/google/play/billing/billing_library_overview
I want to implement consumable in-app purchase. I am using 'android.test.purchased' reserved id for testing. I could load skuDetails and make purchase successfully and consume purchase successfully
here is my handlePurchase method with consumeAsync
void handlePurchase(Purchase purchase) {
BillingClient client = BillingClient.newBuilder(NewAdActivity.this)
.enablePendingPurchases()
.setListener(this)
.build();
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
System.out.println("item successfully purchased");
if (!purchase.isAcknowledged()) {
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
ConsumeResponseListener consumeResponseListener = new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchaseToken != null) {
System.out.println("SUCCESSFULLY consumed PURCHASE");
providecontent();
}
else {
System.out.println("FAILED TO consume:”);
}
}
};
client.consumeAsync(consumeParams, consumeResponseListener);
}
}
}
Does it also acknowledge purchase when I consume purchase? Do I need to set "acknowledged":true in purchase.originalJson manually?
Is my code correct to consume purchased item? or I need to include a separate acknowledgePurchase before consuming item.
Please reply. Any help is truly appreciated.
Thanks.

For consumable products, you want to use consumeAsync(). For products that aren't consumable, you want to use acknowledgePurchase(). For more about acknowledging purchases in your app, check out the official documentation: https://developer.android.com/google/play/billing/billing_library_overview#acknowledge

Welcome to stackoverflow !
You are incorrectly using consumeAsync(), to acknowledge a purchase you should call acknowledgePurchase().
consumeAsync() removes the item purchased, for example if the purchase allows the user to play 10 times and he uses all of them then you would call consumeAsync() to let the user to buy the item again
An example:
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
// Grant entitlement to the user.
boolean signOk = verifyPurchaseSignature(purchase.getOriginalJson(), purchase.getSignature());
if (!signOk) {
// Alert the user about wrong signature
return;
} else if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
#Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
//Give thanks for the purchase
}
});
}
}

Related

onPurchasesUpdated called multiple times

I have set up in-app billing on an Android app (java). When I call launchBillingFlow on the BillingClient:
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//the system displays the Google Play purchase screen
} else {
Log.e(TAG, "Billing failed: + " + billingResult.getDebugMessage());
}
This is what my onPurchasesUpdated (from PurchasesUpdatedListener) looks like:
#Override
public void onPurchasesUpdated(#NonNull BillingResult billingResult, #Nullable List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
for (String sku : purchase.getSkus()) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (skuConsumables.contains(sku)) {
handlePurchaseConsumable(purchase);
} else if (skuNonconsumables.contains(sku)) {
handlePurchaseNonconsumable(purchase);
}
}
}
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
billingServiceListener.receivePurchaseError();
} else {
// Handle any other error codes.
billingServiceListener.receivePurchaseError();
}
}
onPurchasesUpdated is called six times, each time with a responseCode of OK. Twice onPurchasesUpdated is called with zero purchases, that's fine. What I am confused about is how to deal with the four times onPurchasesUpdated is called with one purchase. And it seems as though each of these four purchase objects are indistinguishable - the same packageName, acknowledged, orderId, productId, purchaseState, purchaseToken, etc.
To complicate things, for consumable in-app billing, (these are consumable) I am then calling ConsumeResponseListener and onConsumeResponse is also returning four times, each time with responseCode of OK.
private void handlePurchaseConsumable(Purchase purchase) {
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Handle the success of the consume operation.
}
}
};
billingClient.consumeAsync(consumeParams, listener);
}
Is this behaviour seen by others? I am using static responses while in development, could this be the reason? If people are seeing this, how do you deal with this - do you keep track of what purchase you have attempted, and then when the first response is returned do you register that the purchase was successful and ignore subsequent times that onPurchasesUpdated returns a purchase if you weren't expecting a payment? Though I've seen that Android permits cash purchases with enablePendingPurchases, so that can't be a solution...
This can happen for 2 reasons.
if you are using an outdated version of the google-billing API.
if you forgot to destroy the billing client at the finish of the activity.
I've checked Samples for Google Play In-app Billing and how I can see the main idea of usage the billing client is to use it with activity/fragment lifecycles. So your billing client should implement DefaultLifecycleObserver. We need it to have connect/disconnect billing client use UI lifecycle. In other case we could have few instances of billing client in the App and few listeners that will be triggered with success purchase in same time. So code should looks like:
Activity/Fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(billingLifecycleManager)
}
BillingLifecycleManager:
class BillingLifecycleManager {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
billingClient = BillingClient.newBuilder(app)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
if (billingClient.isReady) {
billingClient.startConnection(...)
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
if (billingClient.isReady) {
billingClient.endConnection()
}
}
}

Android Google Play Billing - querySkuDetailsAsync resurning empty list

I have an Android project where I want to use com.android.billingclient.api version 4.0.0, which would replace an old billing library that google doesn't allow any more (com.anjlab.android.iab.v3). I've implemented the methods for a one-time purchase, but when querying the SKU Details with billingClient.querySkuDetailsAsync using the SKU string for the product, I get an empty result set. I've been assured that the SKU is correct, so I don't know where the error might be.
Also, the old implementation required to provide a license key, which isn't the case with the new library. Do I need to define it somewhere else in the app?
Here's the code where it fails:
List<String> skuList = new ArrayList<>();
skuList.add(SKU_ID);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
final Activity v = this;
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
Has anyone a suggestion what to do?
This is how I query the SKU details within my app.
You can try to use this example and see if this works for you.
billingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#NonNull BillingResult billingResult) {
Log.d(TAG, "Connection finished");
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
List<String> skuList = new ArrayList<> ();
skuList.add(ITEM_SKU_ADREMOVAL);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
(billingResult1, skuDetailsList) -> {
// Process the result.
if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
for (Object skuDetailsObject : skuDetailsList) {
skuDetails = (SkuDetails) skuDetailsObject;
String sku = skuDetails.getSku();
String price = skuDetails.getPrice();
if (ITEM_SKU_ADREMOVAL.equals(sku)) {
removeadsPrice = price;
}
else {
Log.d(TAG,"Sku is null");
}
}
Log.d(TAG, "i got response");
Log.d(TAG, String.valueOf(billingResult1.getResponseCode()));
Log.d(TAG, billingResult1.getDebugMessage());
}
else if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.ERROR) {
Toast.makeText(MainActivity.this, "Error in completing the purchase!", Toast.LENGTH_SHORT).show();
}
});
}
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_TIMEOUT) {
Toast.makeText(MainActivity.this, "Service timeout!", Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(MainActivity.this, "Failed to connect to the billing client!", Toast.LENGTH_SHORT).show();
}
}
#Override
public void onBillingServiceDisconnected() {
restartConnection();
}
});
Getting this one working properly depends on several different factors
Have you published your app to Play Console or at least to an
internal track or something?
Do you have active products or subscriptions on your Google Play
Console?
Have you configured your licensed testers?
Please see the documentation for more info.

how to integrate with Zoom app using android?

I'm new with zoom integration.
I wants user login and create meeting in their account. I've done login user part using loginWithZoom method but now wants to create meeting for that auth token needed.
How can I get token when user login in zoom without OAuth?
I've found but not getting much idea. I tried with JWT token it works with
https://api.zoom.us/v2/users/me/meetings api. I gave Authorization token and content-type in
headers. it gives me all meetings of that specific user. but problem to get different authorization token for different users. I don't have idea is it possible or not.
Suggest if anyone knows
Code I've used for Login:
public void initializeSdk(Context context) {
ZoomSDK sdk = ZoomSDK.getInstance();
// TODO: Do not use hard-coded values for your key/secret in your app in production!
ZoomSDKInitParams params = new ZoomSDKInitParams();
params.appKey = "a...t4.."; // TODO: Retrieve your SDK key and enter it here
params.appSecret = "y...19"; // TODO: Retrieve your SDK secret and enter it here
params.domain = "zoom.us";
params.enableLog = true;
// TODO: Add functionality to this listener (e.g. logs for debugging)
ZoomSDKInitializeListener listener = new ZoomSDKInitializeListener() {
/**
* #param errorCode {#link us.zoom.sdk.ZoomError#ZOOM_ERROR_SUCCESS} if the SDK has been initialized successfully.
*/
#Override
public void onZoomSDKInitializeResult(int errorCode, int internalErrorCode) {
Log.i("","onZoomSDKInitializeResult Error code"+errorCode);
Toast.makeText(getApplicationContext()," error code : " + errorCode,Toast.LENGTH_LONG).show();
}
#Override
public void onZoomAuthIdentityExpired() {
System.out.println(" identity expired..");
}
};
sdk.initialize(context, listener, params);
}
findViewById(R.id.login_button).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Toast.makeText(getApplicationContext(), "onclick of login", Toast.LENGTH_LONG).show();
Log.i(" ","onclick of login : "+ ZoomSDK.getInstance().isLoggedIn());
if (ZoomSDK.getInstance().isLoggedIn()) {
//wants to create meeting
} else {
createLoginDialog();
}
}
});
private void createLoginDialog() {
new AlertDialog.Builder(this)
.setView(R.layout.dialog_login)
.setPositiveButton("Log in", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
AlertDialog dialog = (AlertDialog) dialogInterface;
TextInputEditText emailInput = dialog.findViewById(R.id.email_input);
TextInputEditText passwordInput = dialog.findViewById(R.id.pw_input);
if (emailInput != null && emailInput.getText() != null && passwordInput != null && passwordInput.getText() != null) {
String email = emailInput.getText().toString();
String password = passwordInput.getText().toString();
if (email.trim().length() > 0 && password.trim().length() > 0) {
login(email, password);
}
}
dialog.dismiss();
}
})
.show();
}
public void login(String username, String password) {
int result = ZoomSDK.getInstance().loginWithZoom(username, password);
if (result == ZoomApiError.ZOOM_API_ERROR_SUCCESS) {
// Request executed, listen for result to start meeting
ZoomSDK.getInstance().addAuthenticationListener(authListener);
}
}
public void onZoomSDKLoginResult(long result) {
if (result == ZoomAuthenticationError.ZOOM_AUTH_ERROR_SUCCESS) {
// Once we verify that the request was successful, we may start the meeting
Toast.makeText(getApplicationContext(), "Login successfully", Toast.LENGTH_SHORT).show();
} else if(result == ZoomAuthenticationError.ZOOM_AUTH_ERROR_USER_NOT_EXIST || result == ZoomAuthenticationError.ZOOM_AUTH_ERROR_WRONG_PASSWORD){
Toast.makeText(getApplicationContext(),"Invalid username or password",Toast.LENGTH_LONG).show();
}
}
Thanks in advance.
I tried with JWT token it works with
https://api.zoom.us/v2/users/me/meetings api. I gave Authorization
token and content-type in headers. it gives me all meetings of that
specific user. but problem to get different authorization token for
different users. I don't have idea is it possible or not.
Assuming these users are not part of the same Zoom account, then no, it is not possible as of 2021-08-28. JWT-based authentication is only for Zoom integration in internal applications/services:
Note: JWT may only be used for internal applications and processes. All apps created for third-party usage must use our OAuth app type.
In this context, "internal" means "only to be used with a single Zoom account." Note that there can be many users under one account (e.g., all employees of Corporation XYZ are part of XYZ's Zoom account). Put differently, you can use a JWT issued for the XYZ Zoom account to access information for all users under the XYZ Zoom account, but if you need data for users that are not part of the XYZ Zoom account, then you need an API Key and API Secret for their Zoom account(s) as well to generate JWTs that you can use to retrieve their data.
If you are building an integration/service that you want to make available to the general public, then you need to use OAuth:
This app can either be installed and managed across an account by
account admins (account-level app) or by users individually
(user-managed app).

Best way to save users subscription with google-in-app billing in my external database?

I'm about to implement google in app payment into my android app and here's the thing:
I have 3 subscriptions levels, let's say noob, normal and pro. My app have a login/signup, besides of google login. I've already implement all the purchase in my app, but now i'm wondering which is the best practice to attach that premium subs payed with google billing, with my users, so i can check each user in the app even if they're sharing a device with the same google account but different subscription level.
*For example: I'm in 'normal' subscription payed with account asd#gmail.com with username MINE21 in my nexus 5
Then my sister buys a 'pro' subscription payed with account she#gmail.com with
username SHE123 in her Galaxy S6, but then she grab my Nexus 5 and
logins with SHE123 but in playstore i'm logged as asd#gmail.com.
At that moment, i want to show all the pro features to her, but i need
to confirm if SHE123 is premium even if that the playstore account is
asd#gmail.com linked with MINE21.*
If i'm not being clear explaining myself please let me know.
Am i right if i get the userid from the user buying the subscription, and saving it in my database with the google account id, token purchase and premium level?
I wanna know how can i do to check the google payment with each user of my database even if they're using another google account in play store.
--My db is a MySQL database handled with PHP--
Regards,
Inrovero
I'm going to put my process payment here:
private void processPayment(final String SKU){
PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
#Override
public void onPurchasesUpdated(#NonNull BillingResult billingResult, #Nullable List<Purchase> list) {
// To be implemented
if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null){
// do Something you want
Log.i(TAG, "Compra exitosa");
// Preparar categoría de premium
premium = "";
if(list.get(0).getSku().equals(SKU_BS)) premium = "BS";
else if(list.get(0).getSku().equals(SKU_BN)) premium = "BN";
else if(list.get(0).getSku().equals(SKU_BP)) premium = "BP";
Toast.makeText(mContext, premium + " adquirido", Toast.LENGTH_LONG).show();
// Sincronizar premium con la DB y suscripción con el usuario
User currentUser = SharedPrefManager.getInstance(mContext).getUser();
int uid = currentUser.getId();
// Datos a sincronizar
AccountIdentifiers accountIdentifiers = list.get(0).getAccountIdentifiers();
String accID = accountIdentifiers.getObfuscatedAccountId();
String token = list.get(0).getPurchaseToken();
// Pasar a la DB el token, accID y uid vinculados con el premium.
// UID // ACCID // TOKEN // PREMIUM //
}else if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED){
// do Something you want
Log.i(TAG, "Compra cancelada");
// Nada? Cancelar all?
}else if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE){
Log.i(TAG, "Item inexistente");
}
}
};
final BillingClient billingClient = BillingClient.newBuilder(mContext)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
billingClient.startConnection(new BillingClientStateListener() {
#Override
public void onBillingSetupFinished(#NonNull BillingResult billingResult) {
if(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
// The BillingClient is ready. You can query purchases here.
List<String> skuList = new ArrayList<>();
skuList.add(SKU);
SkuDetailsParams params = SkuDetailsParams.newBuilder()
.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
.build();
billingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
#Override
public void onSkuDetailsResponse(#NonNull BillingResult billingResult, #Nullable List<SkuDetails> list) {
if(list == null){
Log.i(TAG, "lista vacía");
return;
}
for(int i = 0; i < list.size(); i++){
Log.i(TAG, String.valueOf(list.get(i)));
}
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(list.get(0))
.build();
int responseCode = billingClient.launchBillingFlow(requireActivity(), billingFlowParams).getResponseCode();
if(responseCode == BillingClient.BillingResponseCode.OK){
// do Something you want
Log.i(TAG, "responseCode OK");
}else if(responseCode == BillingClient.BillingResponseCode.ERROR){
Log.i(TAG, "responseCode ERROR");
}else if(responseCode == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE){
Log.i(TAG, "responseCode ITEM_UNAVAILABLE");
}
}
});
}
}
#Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
}
When purchase is sucsses send http request to your php server and put the subscription az int
For example int if noob=1 if pro=2
And save it in your database on your server
Than you can create http request from your php to get that number so you know what subscription user have
You can even set string for more understandable form
So if user x have int = 1 you know that is noob subcription
Than do what you want
I hope it will help you figure out

Prompt Android App User to Update App if current version <> market version

Lets say my Android App version 0.1 is installed currently on the User's phone. Everytime they launch my App I want to check if there is a different version available in the Android Market let's say this version is 0.2. If there is a mismatch between these two version I want to show a dialog box prompting the user to Upgrade the App.
I totally understand there exists a notification procedure from Android Market itself to the users but as far as my Analytics data is concerned it is not very effective in reminding users to upgrade to the new version of the App.
Any insight would be very helpful. Thanks StackOverflowers, you guys rock!
As of 2019 the best way for updating your app is to use In-app updates provided by Play Core library (1.5.0+). It works for Lollipop and newer, but let's be fair, Kit-Kat is less than 7% as of today and soon will be gone forever. You can safely run this code on Kit-Kat without version checks, it won't crash.
Official documentation: https://developer.android.com/guide/app-bundle/in-app-updates
There are two types of In-app updates: Flexible and Immediate
Flexible will ask you nicely in a dialog window:
whereas Immediate will require you to update the app in order to continue using it with full-screen message (this page can be dismissed):
Important: for now, you can't choose which type of update to roll out in your App Release section on Developer Play Console. But apparently, they will give us that option soon.
From what I've tested, currently, we're getting both types available in onSuccessListener.
So let's implement both types in our code.
In module build.gradle add the following dependency:
dependencies {
implementation 'com.google.android.play:core:1.6.1'//for new version updater
}
In MainActivity.class:
private static final int REQ_CODE_VERSION_UPDATE = 530;
private AppUpdateManager appUpdateManager;
private InstallStateUpdatedListener installStateUpdatedListener;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkForAppUpdate();
}
#Override
protected void onResume() {
super.onResume();
checkNewAppVersionState();
}
#Override
public void onActivityResult(int requestCode, final int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
switch (requestCode) {
case REQ_CODE_VERSION_UPDATE:
if (resultCode != RESULT_OK) { //RESULT_OK / RESULT_CANCELED / RESULT_IN_APP_UPDATE_FAILED
L.d("Update flow failed! Result code: " + resultCode);
// If the update is cancelled or fails,
// you can request to start the update again.
unregisterInstallStateUpdListener();
}
break;
}
}
#Override
protected void onDestroy() {
unregisterInstallStateUpdListener();
super.onDestroy();
}
private void checkForAppUpdate() {
// Creates instance of the manager.
appUpdateManager = AppUpdateManagerFactory.create(AppCustom.getAppContext());
// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
// Create a listener to track request state updates.
installStateUpdatedListener = new InstallStateUpdatedListener() {
#Override
public void onStateUpdate(InstallState installState) {
// Show module progress, log state, or install the update.
if (installState.installStatus() == InstallStatus.DOWNLOADED)
// After the update is downloaded, show a notification
// and request user confirmation to restart the app.
popupSnackbarForCompleteUpdateAndUnregister();
}
};
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
// Request the update.
if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
// Before starting an update, register a listener for updates.
appUpdateManager.registerListener(installStateUpdatedListener);
// Start an update.
startAppUpdateFlexible(appUpdateInfo);
} else if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) ) {
// Start an update.
startAppUpdateImmediate(appUpdateInfo);
}
}
});
}
private void startAppUpdateImmediate(AppUpdateInfo appUpdateInfo) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
// The current activity making the update request.
this,
// Include a request code to later monitor this update request.
MainActivity.REQ_CODE_VERSION_UPDATE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
private void startAppUpdateFlexible(AppUpdateInfo appUpdateInfo) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
// The current activity making the update request.
this,
// Include a request code to later monitor this update request.
MainActivity.REQ_CODE_VERSION_UPDATE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
unregisterInstallStateUpdListener();
}
}
/**
* Displays the snackbar notification and call to action.
* Needed only for Flexible app update
*/
private void popupSnackbarForCompleteUpdateAndUnregister() {
Snackbar snackbar =
Snackbar.make(drawerLayout, getString(R.string.update_downloaded), Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.restart, new View.OnClickListener() {
#Override
public void onClick(View view) {
appUpdateManager.completeUpdate();
}
});
snackbar.setActionTextColor(getResources().getColor(R.color.action_color));
snackbar.show();
unregisterInstallStateUpdListener();
}
/**
* Checks that the update is not stalled during 'onResume()'.
* However, you should execute this check at all app entry points.
*/
private void checkNewAppVersionState() {
appUpdateManager
.getAppUpdateInfo()
.addOnSuccessListener(
appUpdateInfo -> {
//FLEXIBLE:
// If the update is downloaded but not installed,
// notify the user to complete the update.
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdateAndUnregister();
}
//IMMEDIATE:
if (appUpdateInfo.updateAvailability()
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
// If an in-app update is already running, resume the update.
startAppUpdateImmediate(appUpdateInfo);
}
});
}
/**
* Needed only for FLEXIBLE update
*/
private void unregisterInstallStateUpdListener() {
if (appUpdateManager != null && installStateUpdatedListener != null)
appUpdateManager.unregisterListener(installStateUpdatedListener);
}
And we're done!
Testing.
Please read the docs so you will know how to test it properly with test tracks on Google Play.
Long story short:
Sign your app with the release certificate and upload it to the one of publishing tracks in Developer Play Console under App Releases (alpha/beta/other custom closed track).
In your release track page in the Manage Testers section create and add a list of testers and make sure you checked the checkbox! - this step is optional since your developer account email is also a testers account and you can use it for testing.
Under the list of testers you will find "Opt-in URL" - copy this url and give it to your testers or open it yourself. Go to that page and accept proposition for testing. There will be a link to the app. (You won't be able to search for the app in Play Store so bookmark it)
Install the app on your device by that link.
In build.gradle increment the version of defaultConfig { versionCode k+1 } and build another signed apk Build > Generate Signed Bundle / APK... and upload it to your publishing track.
Wait for... 1 hour? 2 hours? or more before it will be published on the track.
CLEAR THE CACHE of Play Store app on your device. The problem is that Play app caches details about installed apps and their available updates so you need to clear the cache. In order to do that take two steps:
7.1. Go to Settings > App > Google PLay Store > Storage > Clear Cache.
7.2. Open the Play Store app > open main menu > My apps & games > and there you should see that your app has a new update.
If you don't see it make sure that your new update is already released on the track (go to your bookmarked page and use it to open your apps listing on the Play Store to see what version is shown there). Also, when your update will be live you'll see a notification on the top right of your Developer Play Console (a bell icon will have a red dot).
Hope it helps.
The Android Market is a closed system and has only an unofficial api that might break at any point of time.
Your best bet is simply to host a file(xml, json or simple text) on a web server of yours in which you just have to update the current version of your app when you post it on the Market.
Your app will then only have to fetch that file at startup, checks wether currently installed app has a lower version number and displays a dialog to warn the user he is lagging.
Another option you can use, if you want to avoid having your backend server to store your current app version like it's suggested in the accepted answer, is to use Google Tag Manager (GTM).
If you're already using the Google Analytics SDK, you have the GTM in it also.
In GTM you can define a value in the container for your app that specifies your latest released version. For example:
{
"latestAppVersion": 14,
...
}
Then you can query that value when your app starts and show the user update dialog reminder if there's a newer version.
Container container = TagManager.getInstance(context).openContainer(myContainerId);
long latestVersionCode = container.getLong("latestAppVersion");
// get currently running app version code
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
long versionCode = pInfo.versionCode;
// check if update is needed
if(versionCode < latestVersionCode) {
// remind user to update his version
}
Take a look at this library that you can use to query the Android Market API
http://code.google.com/p/android-market-api/
You can use this Android Library: https://github.com/danielemaddaluno/Android-Update-Checker. It aims to provide a reusable instrument to check asynchronously if exists any newer released update of your app on the Store.
It is based on the use of Jsoup (http://jsoup.org/) to test if a new update really exists parsing the app page on the Google Play Store:
private boolean web_update(){
try {
String curVersion = applicationContext.getPackageManager().getPackageInfo(package_name, 0).versionName;
String newVersion = curVersion;
newVersion = Jsoup.connect("https://play.google.com/store/apps/details?id=" + package_name + "&hl=en")
.timeout(30000)
.userAgent("Mozilla/5.0 (Windows; U; WindowsNT 5.1; en-US; rv1.8.1.6) Gecko/20070725 Firefox/2.0.0.6")
.referrer("http://www.google.com")
.get()
.select("div[itemprop=softwareVersion]")
.first()
.ownText();
return (value(curVersion) < value(newVersion)) ? true : false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
And as "value" function the following (works if values are beetween 0-99):
private long value(String string) {
string = string.trim();
if( string.contains( "." )){
final int index = string.lastIndexOf( "." );
return value( string.substring( 0, index ))* 100 + value( string.substring( index + 1 ));
}
else {
return Long.valueOf( string );
}
}
If you want only to verify a mismatch beetween versions, you can change:
"value(curVersion) < value(newVersion)" with "value(curVersion) != value(newVersion)"
For prompting Android App User to Update App if current version is not equal to market version, you should first check the app version on the market and compare it with the version of the app on the device. If they are different, it may be an update available. In this post I wrote down the code for getting the current version of market and current version on the device and compare them together. I also showed how to show the update dialog and redirect the user to the update page. Please visit this link: https://stackoverflow.com/a/33925032/5475941
My working Kotlin code for force App update:
const val FLEXIABLE_UPADTE: Int = 101
const val FORCE_UPDATE: Int = 102
const val APP_UPDATE_CODE: Int = 500
override fun onCreate {
// Get updateType from Webservice.
updateApp(updateType)
}
private fun updateApp(statusCode: Int) {
appUpdateManager = AppUpdateManagerFactory.create(this #MainActivity)
val appUpdateInfoTask = appUpdateManager ? .appUpdateInfo
appUpdateInfoTask ? .addOnSuccessListener {
appUpdateInfo - >
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
if ((statusCode == Constants.FORCE_UPDATE))
appUpdateManager ? .startUpdateFlowForResult(
appUpdateInfo, AppUpdateType.IMMEDIATE, this, Constants.APP_UPDATE_CODE
)
else if (statusCode == Constants.FLEXIABLE_UPADTE)
appUpdateManager ? .startUpdateFlowForResult(
appUpdateInfo, AppUpdateType.FLEXIBLE, this, Constants.FLEXIABLE_UPADTE
)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent ? ) {
try {
if (requestCode == Constants.APP_UPDATE_CODE && resultCode == Activity.RESULT_OK) {
if (resultCode != RESULT_OK) {
appUpdateCompleted()
}
}
} catch (e: java.lang.Exception) {
}
}
private fun appUpdateCompleted() {
Snackbar.make(
findViewById(R.id.activity_main_layout),
"An update has just been downloaded.",
Snackbar.LENGTH_INDEFINITE
).apply {
setAction("RESTART") {
appUpdateManager.completeUpdate()
}
setActionTextColor(resources.getColor(R.color.snackbar_action_text_color))
show()
}
}

Categories

Resources