Pull to refresh

Comments 22

Не в обиду будет сказано, но — ох уж эти велосипеды. Лучше бы github.com/onepf/OpenIAB поддержали, баги там пофиксили.
Всегда удивляет что вместо того чтобы хотя бы попробовать доработать уже готовый, но может, не совсем доделанный велосипед все пишут свой.
Не в обиду будет сказано, но:
1. Это не эффектиный и даже неправильно работающий код — новый поток создаётся каждый раз, `flagEndAsync` находится не в finally блоке, доступ к `setupState` не синхронизован:
      (new Thread(new Runnable() {
            public void run() {
                final List<IabResult> results = new ArrayList<IabResult>();
                for (Purchase purchase : purchases) {
                    try {
                        consume(purchase);
                        results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
                    } catch (IabException ex) {
                        results.add(ex.getResult());
                    }
                }

                flagEndAsync();
                if (setupState != SETUP_DISPOSED && singleListener != null) {
                    notifyHandler.post(new Runnable() {
                        public void run() {
                            singleListener.onConsumeFinished(purchases.get(0), results.get(0));
                        }
                    });
                }
                if (setupState != SETUP_DISPOSED && multiListener != null) {
                    notifyHandler.post(new Runnable() {
                        public void run() {
                            multiListener.onConsumeMultiFinished(purchases, results);
                        }
                    });
                }
            }
        })).start();

2. Судя по всему, OpenIAB не работает в многопоточном окружении (Billing из моей библиотеки thread safe)
3. Использование переменных `mAsyncInProgress` и `mAsyncOperation` без синхронизации, вы серьёзно?
4. Где тесты?
И это только при беглом осмотре одного класса, который, кстати, несколько великоват для его нормальной поддержки (~2k строк кода).

Подводя итог, я бы не стал использовать OpenAIB в своих проектах, так как качество кода оставляет желать лучшего.
Так я о том и говорю, возьмите и улучшите. Зачем увеличивать количество одинаковых по функционалу проектов? Чтобы потом те кто хочет этими библиотеками воспользоваться увидели этот зоопарк и у них глаза разбегались? К сожалению, практика показывает что все такие проекты начинаются очень бодро и хорошо, а потом пыл угас, в поддержку закапываться не хочется и выходит что есть пара десятков разных библиотек, каждая из которых брошена на каком-то этапе разработки.

В целом, я с Вами согласен код в OpenIAB не супер, но он ведь поддается рефакторингу, да и тесты можно написать.
Я с вами согласен — помогать проектам надо, свои велосипеды обычно не нужны. Но в данном конкретном случае слишком много различий, например, абсолютно разный API. Вы же не хотите, чтобы у всех кто используют старый API поломались сборки после обновления?

Про поддержку проекта — библиотека работает уже сейчас, расширяться особо нечему. Нужно только будет фиксить баги, но в этом я заинтересован сам, так как использую библиотеку в своих приложениях.
А стандартные Google Service библиотека чем не устраивает?
По моим впечатлениям, объем кода для интеграции примерно такой же.
Что за стандартная Google Service библиотека? Код из семплов?

Вот список того, что библиотека берёт на себя:
1. Асинхронное подключение IInAppBillingService. Если на момент выполнения биллинг запроса сервис не подключён — запрос встаёт в очередь и ждёт подключения.
2. Многопоточность — некоторые запросы должны выполняться на отдельном потоке, например, getSkuDetails или consumePurchase (см. документацию).
3. Проверка на поддержку биллинга.
4. Типовые задачи — парсинг json, обработка ошибок, действия нужные для покупки и т.д.
5. Отмена запросов (и слушателей). Полезно, например, при закрытии Activity.
6. Возможность выполнять запросы не из Activity.
7. Кеширование результатов.
8. Код Activity становится проще за счёт того, что код биллинга находится в отдельных классах.
Да, проглядел код этой библиотеки перед написанием своей. Есть ряд проблем, например:
1. BillingProcessor работает только с Activity. Т.е. загрузить данные о покупках, например, в Application или Service просто невозможно.
2. Каждый раз при старте Activity происходит переподключение биллинг сервиса, что не эффективно, т.к. Activity убивается каждый раз при повороте экрана (у меня он подключается один раз при старте приложения).
3. BillingProcessor может вызвать методы слушателя тогда, когда это не нужно. Самый простой пример — нужно загрузить список продуктов или купить продукт в Activity. Если пользователь повернёт экран, то Activity убивается, но слушатель, который был создан остаётся. При вызове методов последнего скорее всего приложение навернётся, т.к. Activity уже разрушено.
4. consumePurchase и другие методы следует вызывать на отдельном потоке (см. доки). В этой библиотеки они вызываются на основном потоке, что может привести к неотзывчивому интерфейсу.
5. Нет юнит тестов. Для библиотеки по работе с деньгами считаю это неприемлемым.

В целом код невысокого качества — это заметно даже после беглого осмотра (форматирование, доки, название переменных и т.д.)
Спасибо за ответ! Сейчас как раз просматриваю библиотеки для реализации биллинга, очень странно, что практически нет готовых решений приемлимого качества, а имплементить самостоятельно не очень хочеться.
А с помощью Вашего приложения можно продавать подписки с пробным периодом?
Добавил annotations.jar из FindBugs в libs, ошибки ушли. Магия
Тоже промучился с настройками студии, пытаясь через них подключить javax.annotation.
Решение лежит в исходнике в конфигурации lib/build.gradle. Только там указана старая версия — «2.0.3». На сейчас последняя «3.0.1».

dependencies {
	...
	compile 'com.google.code.findbugs:jsr305:3.0.1'
}


Плюс у меня ругнулось на отсутствие DEBUG_MODE. Тоже добавил в build.gradle:

buildTypes {
	debug {
		buildConfigField "Boolean", "DEBUG_MODE", "true"
	}
	release {
		buildConfigField "Boolean", "DEBUG_MODE", "false"
	}
}
Можно было просто добавить transitive = true для зависимости
Grandle для меня сейчас лес густой. Речь о прописанной старой версии findbugs в зависимостях/dependencies?

Вот такая конструкция c «transitive = true» загрузила всё равно 2.0.3, а не последнюю — 3.0.1.

compile ('com.google.code.findbugs:jsr305:2.0.3') {
	transitive = true
}
Нет, о библиотечной зависимости:
compile ('org.solovyev.android:checkout:x.x.x@aar') {
    transitive = true
}

Всё дело в том, что если в Gradle у зависимости указать классификатор (classifier, aar в данном случае), то свойство «transitive» будет выставлено в false, т.е. зависимости зависимостей не подтянутся автоматически.
А не подскажете как воспользоваться вашей библиотекой в виджете?
Сейчас я на кнопку виджета «купи меня» вешаю
intent который запускает пустую activity с
кодом покупки и получением информации что покупка
была совершена через inventory.
(вызовы вашей библиотеки по примеру для activity).

Но как-то это не достаточно красиво получается.
Это, похоже, единственный способ совершить покупку из виджета. Если вы не хотите, чтобы пользователь переходил на дополнительный экран, можете сделать активити прозрачной
Спасибо за ответ и за саму библиотеку, да, activity естественно прозрачная.
Обновил в работающем проекте play-services с 10.0.1 на 10.2.0. Обработчик загрузки инвентаря onLoaded перестал вызываться. Checkout.start отрабатывает нормально. Библиотека полугодовалой давности.

Вот так было и работало:
mCheckout.start();

mCheckout.createPurchaseFlow(new PurchaseListener());

mInventory = mCheckout.loadInventory();
mInventory.whenLoaded(new InventoryLoadedListener());


Посмотрел на гитхабе как сейчас предлагаете делать:
mCheckout.start();

mCheckout.createPurchaseFlow(new PurchaseListener());

mInventory = mCheckout.makeInventory();
mInventory.load(Inventory.Request.create()
        .loadAllPurchases()
        .loadSkus(ProductTypes.IN_APP, "sku_01"), new InventoryCallback());


А в статье на медиуме даёте ещё один способ (смесь двух первых), там почему-то нет createPurchaseFlow. Есть startPurchaseFlow в покупке. И непонятно, — необходимо создавать поток сразу после старта (как на гитхабе) или нет (как в новой статье).

Честно говоря, десяток способов сделать одно и тоже только запутывают.

Не нравится эта тишина с InventoryLoadedListener. Если бы вываливалось с ошибкой, можно было бы найти что не так. Без отладки (приложения для теста платежей теперь надо заливать в плей и без включенной отладки) и на логах Stetho найти проблему самому не получается.
Вот шаблон кода, работающего сейчас с 10.2.0.
шаблон покупки
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;

import org.solovyev.android.checkout.ActivityCheckout;
import org.solovyev.android.checkout.BillingRequests;
import org.solovyev.android.checkout.Checkout;
import org.solovyev.android.checkout.EmptyRequestListener;
import org.solovyev.android.checkout.Inventory;
import org.solovyev.android.checkout.ProductTypes;
import org.solovyev.android.checkout.Purchase;
import org.solovyev.android.checkout.RequestListener;
import org.solovyev.android.checkout.ResponseCodes;
import org.solovyev.android.checkout.Sku;

import java.util.Locale;

import javax.annotation.Nonnull;


public abstract class InAppTemplateActivity extends Activity {

	public final static String SKU = "sku_1";

	private Inventory.Request mInventoryRequest;

	private final InventoryCallback mInventoryCallback = new InventoryCallback();

	private ActivityCheckout mCheckout;

	private Inventory mInventory;


	@Override
	protected void onCreate(@Nullable Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);


		mCheckout = Checkout.forActivity(this, application.inApp.getBilling());

		mCheckout.start(new Checkout.Listener() {
			public void onReady(@Nonnull BillingRequests requests) {
				Log.i(this.getClass().toString(), "Checkout onReady");
			}

			public void onReady(@Nonnull BillingRequests requests, @Nonnull String product, boolean billingSupported) {
			}
		});

		mCheckout.createPurchaseFlow(new PurchaseListener());

		mInventory = mCheckout.makeInventory();


		mInventoryRequest = Inventory.Request.create();
		// load purchase info
		mInventoryRequest.loadAllPurchases();
		// load SKU details
		mInventoryRequest.loadSkus(ProductTypes.IN_APP, "android.test.purchased", SKU);


		reloadInventory();
	}

	@Override
	protected void onDestroy() {
		mCheckout.stop();
		mCheckout = null;

		mInventory = null;

		super.onDestroy();
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		mCheckout.onActivityResult(requestCode, resultCode, data);
		super.onActivityResult(requestCode, resultCode, data);
	}

	protected void purchase(final String sku) {
		mCheckout.whenReady(new Checkout.EmptyListener() {
			@Override
			public void onReady(BillingRequests requests) {
				requests.purchase(ProductTypes.IN_APP, sku, null, mCheckout.getPurchaseFlow());
			}
		});
	}

	private void reloadInventory() {
		mInventory.load(mInventoryRequest, mInventoryCallback);
	}

	private void consume(final Purchase purchase) {
		mCheckout.whenReady(new Checkout.EmptyListener() {
			@Override
			public void onReady(@Nonnull BillingRequests requests) {
				requests.consume(purchase.token, new RequestListener<Object>() {
					@Override
					public void onSuccess(@Nonnull Object result) {
						Log.i(this.getClass().toString(), "Consume onSuccess");

						consumed();

						reloadInventory();
					}

					@Override
					public void onError(int response, @Nonnull Exception e) {
						// it is possible that our data is not synchronized with data on Google Play => need to handle some errors
						if (response == ResponseCodes.ITEM_NOT_OWNED) {
							Log.i(this.getClass().toString(), "ERROR: ITEM_NOT_OWNED");

							consumed();
						} else {
							Log.e(this.getClass().toString(), "ERROR: " + e.toString());
						}

						reloadInventory();
					}

					private void consumed() {
						// если нужно, сделать что-то после потребления покупки
					}
				});
			}
		});
	}

	// Чтобы в оплате проходили Static Responses (у них нет токена) нужно заменить дефолтный PurchaseVerifier на свой,
	// например, такой:
/*	class MyPurchaseVerifier implements PurchaseVerifier {

		@Nonnull
		private final String mPublicKey;

		public MyPurchaseVerifier(@Nonnull String publicKey) {
			mPublicKey = publicKey;
		}

		@Override
		public void verify(@Nonnull List<Purchase> purchases, @Nonnull RequestListener<List<Purchase>> listener) {
			final List<Purchase> verifiedPurchases = new ArrayList<Purchase>(purchases.size());
			for (Purchase purchase : purchases) {
				if ("android.test.purchased;android.test.canceled;android.test.refunded;android.test.item_unavailable".contains(purchase.sku)) {
					verifiedPurchases.add(purchase);
				} else if (Security.verifyPurchase(mPublicKey, purchase.data, purchase.signature)) {
					verifiedPurchases.add(purchase);
				} else {
					if (isEmpty(purchase.signature)) {
						Log.e(this.getClass().toString(), "Cannot verify purchase: " + purchase + ". Signature is empty");
					} else {
						Log.e(this.getClass().toString(), "Cannot verify purchase: " + purchase + ". Wrong signature");
					}
				}
			}
			listener.onSuccess(verifiedPurchases);
		}
	}*/
	// И вернуть его в new Billing.DefaultConfiguration() 
	/*
	@Nonnull
	@Override
	public PurchaseVerifier getPurchaseVerifier() {
		return new MyPurchaseVerifier(getPublicKey());
	}
	*/

	private class PurchaseListener extends EmptyRequestListener<Purchase> {
		@Override
		public void onSuccess(@Nonnull Purchase purchase) {
			Log.i(this.getClass().toString(), "Purchase onSuccess");

			purchased();

			reloadInventory();
		}

		@Override
		public void onError(int response, @Nonnull Exception e) {
			// it is possible that our data is not synchronized with data on Google Play => need to handle some errors
			if (response == ResponseCodes.ITEM_ALREADY_OWNED) {
				Log.i(this.getClass().toString(), "ERROR: ITEM_ALREADY_OWNED");

				purchased();
			} else {
				Log.e(this.getClass().toString(), "ERROR: " + e.toString());
			}

			reloadInventory();
		}

		private void purchased() {
			// сделать что-то после покупки
		}
	}

	private class InventoryCallback implements Inventory.Callback {
		@Override
		public void onLoaded(Inventory.Products products) {
			final Inventory.Product product = products.get(ProductTypes.IN_APP);

			if (!product.supported) {
				// billing is not supported, user can't purchase anything. Don't show ads in this case
				Log.e(this.getClass().toString(), "billing_not_supported");
				return;
			}

			for (Sku sku : product.getSkus()) {
				Log.i(this.getClass().toString(),
						String.format(Locale.US, "SKU: id = %s, title = %s, price = %s, currency = %s",
								sku.id,
								sku.title,
								sku.detailedPrice.amount/1000000,
								sku.detailedPrice.currency
						)
				);

				final Purchase purchase = product.getPurchaseInState(sku, Purchase.State.PURCHASED);
				if (purchase != null) {
					Log.i(this.getClass().toString(), String.format(Locale.US, "Куплено - SKU: id = %s", sku.id));

					if (purchase.token != null) {
						// token есть только у непотреблённых покупок

						Log.i(this.getClass().toString(),
								String.format(Locale.US, "Необходимо потребить товар - SKU: id = %s, token = %s, Google Wallet Order ID = %s, payload = %s",
										sku.id,
										purchase.token,
										purchase.orderId != null ? purchase.orderId : "null",// У static responses orderId отсутствует
										purchase.payload
								)
						);

						consume(purchase);
					}
				}
			}
		}
	}
}

Не увидел вашего вопроса вовремя. Purchase flow необходим только при совершении покупки, для загрузки инвентаря он не нужен. В одной из последних версий либы для удобства отладки был добавлен Logger, который можно выставить через Billing#setLogger.
Sign up to leave a comment.

Articles