android: Implement billing for Premium.
This commit is contained in:
parent
9ee1489524
commit
73fc4515be
@ -119,4 +119,6 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
|
||||
|
||||
implementation 'com.android.billingclient:billing:2.2.0'
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
|
||||
public final class PremiumHeader extends SettingsItem {
|
||||
public PremiumHeader() {
|
||||
super(null, null, null, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return SettingsItem.TYPE_PREMIUM;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package org.citra.citra_emu.features.settings.model.view;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.Setting;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
|
||||
/**
|
||||
@ -19,6 +20,7 @@ public abstract class SettingsItem {
|
||||
public static final int TYPE_INPUT_BINDING = 5;
|
||||
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
|
||||
public static final int TYPE_DATETIME_SETTING = 7;
|
||||
public static final int TYPE_PREMIUM = 8;
|
||||
|
||||
private String mKey;
|
||||
private String mSection;
|
||||
@ -27,6 +29,7 @@ public abstract class SettingsItem {
|
||||
|
||||
private int mNameId;
|
||||
private int mDescriptionId;
|
||||
private boolean mIsPremium;
|
||||
|
||||
/**
|
||||
* Base constructor. Takes a key / section name in case the third parameter, the Setting,
|
||||
@ -45,6 +48,7 @@ public abstract class SettingsItem {
|
||||
mSetting = setting;
|
||||
mNameId = nameId;
|
||||
mDescriptionId = descriptionId;
|
||||
mIsPremium = (section == Settings.SECTION_PREMIUM);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,6 +93,10 @@ public abstract class SettingsItem {
|
||||
return mDescriptionId;
|
||||
}
|
||||
|
||||
public boolean isPremium() {
|
||||
return mIsPremium;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by {@link SettingsAdapter}'s onCreateViewHolder()
|
||||
* method to determine which type of ViewHolder should be created.
|
||||
|
@ -30,10 +30,12 @@ import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHo
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
|
||||
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -92,6 +94,9 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
||||
view = inflater.inflate(R.layout.list_item_setting, parent, false);
|
||||
return new DateTimeViewHolder(view, this);
|
||||
|
||||
case SettingsItem.TYPE_PREMIUM:
|
||||
view = inflater.inflate(R.layout.premium_item_setting, parent, false);
|
||||
return new PremiumViewHolder(view, this, mView);
|
||||
|
||||
default:
|
||||
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
|
||||
@ -138,9 +143,8 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
||||
mView.onSettingChanged();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item) {
|
||||
mClickedItem = item;
|
||||
mClickedPosition = position;
|
||||
|
||||
int value = getSelectionForSingleChoiceValue(item);
|
||||
|
||||
@ -152,6 +156,19 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
||||
mDialog = builder.show();
|
||||
}
|
||||
|
||||
public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
|
||||
mClickedPosition = position;
|
||||
|
||||
if (!item.isPremium() || MainActivity.isPremiumActive()) {
|
||||
// Setting is either not Premium, or the user has Premium
|
||||
onSingleChoiceClick(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// User needs Premium, invoke the billing flow
|
||||
MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item));
|
||||
}
|
||||
|
||||
public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
|
||||
mClickedItem = item;
|
||||
mClickedPosition = position;
|
||||
|
@ -16,6 +16,7 @@ import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SliderSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
|
||||
import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -130,6 +131,7 @@ public final class SettingsFragmentPresenter {
|
||||
SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM);
|
||||
Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN);
|
||||
|
||||
sl.add(new PremiumHeader());
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
package org.citra.citra_emu.features.settings.ui.viewholder;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.view.SettingsItem;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
|
||||
public final class PremiumViewHolder extends SettingViewHolder {
|
||||
private TextView mHeaderName;
|
||||
private TextView mTextDescription;
|
||||
private SettingsFragmentView mView;
|
||||
|
||||
public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) {
|
||||
super(itemView, adapter);
|
||||
mView = view;
|
||||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void findViews(View root) {
|
||||
mHeaderName = root.findViewById(R.id.text_setting_name);
|
||||
mTextDescription = root.findViewById(R.id.text_setting_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SettingsItem item) {
|
||||
updateText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clicked) {
|
||||
if (MainActivity.isPremiumActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke billing flow if Premium is not already active, then refresh the UI to indicate
|
||||
// the purchase has completed.
|
||||
MainActivity.invokePremiumBilling(() -> updateText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text shown to the user, based on whether Premium is active
|
||||
*/
|
||||
private void updateText() {
|
||||
if (MainActivity.isPremiumActive()) {
|
||||
mHeaderName.setText(R.string.premium_settings_welcome);
|
||||
mTextDescription.setText(R.string.premium_settings_welcome_description);
|
||||
} else {
|
||||
mHeaderName.setText(R.string.premium_settings_upsell);
|
||||
mTextDescription.setText(R.string.premium_settings_upsell_description);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||
import org.citra.citra_emu.model.GameProvider;
|
||||
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||
import org.citra.citra_emu.utils.BillingManager;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
@ -35,6 +36,9 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||
|
||||
private MainPresenter mPresenter = new MainPresenter(this);
|
||||
|
||||
// Singleton to manage user billing state
|
||||
private static BillingManager mBillingManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -61,6 +65,9 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
|
||||
}
|
||||
|
||||
// Setup billing manager, so we can globally query for Premium status
|
||||
mBillingManager = new BillingManager(this);
|
||||
|
||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||
EmulationActivity.tryDismissRunningNotification(this);
|
||||
}
|
||||
@ -194,4 +201,20 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||
EmulationActivity.tryDismissRunningNotification(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if Premium subscription is currently active
|
||||
*/
|
||||
public static boolean isPremiumActive() {
|
||||
return mBillingManager.isPremiumActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the billing flow for Premium
|
||||
*
|
||||
* @param callback Optional callback, called once, on completion of billing
|
||||
*/
|
||||
public static void invokePremiumBilling(Runnable callback) {
|
||||
mBillingManager.invokePremiumBilling(callback);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ package org.citra.citra_emu.ui.main;
|
||||
import org.citra.citra_emu.BuildConfig;
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.features.settings.model.Settings;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||
@ -33,6 +34,10 @@ public final class MainPresenter {
|
||||
case R.id.button_add_directory:
|
||||
mView.launchFileListActivity();
|
||||
return true;
|
||||
|
||||
case R.id.button_premium:
|
||||
mView.launchSettingsActivity(Settings.SECTION_PREMIUM);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -0,0 +1,167 @@
|
||||
package org.citra.citra_emu.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
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.Purchase.PurchasesResult;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BillingManager implements PurchasesUpdatedListener {
|
||||
private final String BILLING_SKU_PREMIUM = "android.test.purchased";
|
||||
|
||||
private final Activity mActivity;
|
||||
private BillingClient mBillingClient;
|
||||
private SkuDetails mSkuPremium;
|
||||
private boolean mIsPremiumActive = false;
|
||||
private boolean mIsServiceConnected = false;
|
||||
private Runnable mUpdateBillingCallback;
|
||||
|
||||
public BillingManager(Activity activity) {
|
||||
mActivity = activity;
|
||||
mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build();
|
||||
querySkuDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if Premium subscription is currently active
|
||||
*/
|
||||
public boolean isPremiumActive() {
|
||||
return mIsPremiumActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the billing flow for Premium
|
||||
*
|
||||
* @param callback Optional callback, called once, on completion of billing
|
||||
*/
|
||||
public void invokePremiumBilling(Runnable callback) {
|
||||
if (mSkuPremium == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional callback to refresh the UI for the caller when billing completes
|
||||
mUpdateBillingCallback = callback;
|
||||
|
||||
// Invoke the billing flow
|
||||
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(mSkuPremium)
|
||||
.build();
|
||||
mBillingClient.launchBillingFlow(mActivity, flowParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) {
|
||||
if (purchaseList == null || purchaseList.isEmpty()) {
|
||||
// Premium is not active, or billing is unavailable
|
||||
return;
|
||||
}
|
||||
|
||||
Purchase premiumPurchase = null;
|
||||
for (Purchase purchase : purchaseList) {
|
||||
if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) {
|
||||
premiumPurchase = purchase;
|
||||
}
|
||||
}
|
||||
|
||||
if (premiumPurchase != null) {
|
||||
// Premium has been purchased
|
||||
mIsPremiumActive = true;
|
||||
|
||||
if (mUpdateBillingCallback != null) {
|
||||
try {
|
||||
mUpdateBillingCallback.run();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
mUpdateBillingCallback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) {
|
||||
if (skuDetailsList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSkuPremium = skuDetailsList.get(0);
|
||||
|
||||
queryPurchases();
|
||||
}
|
||||
|
||||
private void querySkuDetails() {
|
||||
Runnable queryToExecute = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
List<String> skuList = new ArrayList<>();
|
||||
|
||||
skuList.add(BILLING_SKU_PREMIUM);
|
||||
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
|
||||
|
||||
mBillingClient.querySkuDetailsAsync(params.build(),
|
||||
(billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList));
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute);
|
||||
}
|
||||
|
||||
private void onQueryPurchasesFinished(PurchasesResult result) {
|
||||
// Have we been disposed of in the meantime? If so, or bad result code, then quit
|
||||
if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
|
||||
return;
|
||||
}
|
||||
// Update the UI and purchases inventory with new list of purchases
|
||||
onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
|
||||
}
|
||||
|
||||
private void queryPurchases() {
|
||||
Runnable queryToExecute = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
|
||||
onQueryPurchasesFinished(purchasesResult);
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute);
|
||||
}
|
||||
|
||||
private void startServiceConnection(final Runnable executeOnFinish) {
|
||||
mBillingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
mIsServiceConnected = true;
|
||||
}
|
||||
|
||||
if (executeOnFinish != null) {
|
||||
executeOnFinish.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
mIsServiceConnected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void executeServiceRequest(Runnable runnable) {
|
||||
if (mIsServiceConnected) {
|
||||
runnable.run();
|
||||
} else {
|
||||
// If billing service was disconnected, we try to reconnect 1 time.
|
||||
startServiceConnection(runnable);
|
||||
}
|
||||
}
|
||||
}
|
42
src/android/app/src/main/res/layout/premium_item_setting.xml
Normal file
42
src/android/app/src/main/res/layout/premium_item_setting.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="72dp"
|
||||
android:paddingTop="@dimen/spacing_large"
|
||||
android:paddingBottom="@dimen/spacing_large">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_setting_name"
|
||||
style="@style/TextAppearance.AppCompat.Headline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:textColor="?android:colorAccent"
|
||||
android:textStyle="bold"
|
||||
tools:text="Setting Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_setting_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/text_setting_name"
|
||||
android:layout_alignStart="@+id/text_setting_name"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:visibility="visible"
|
||||
tools:text="@string/app_disclaimer" />
|
||||
|
||||
</RelativeLayout>
|
@ -2,12 +2,15 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/button_premium"
|
||||
android:title="@string/premium_text"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/button_add_directory"
|
||||
android:icon="@drawable/ic_folder"
|
||||
android:title="@string/add_directory_title"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_settings_core"
|
||||
android:icon="@drawable/ic_settings_core"
|
||||
|
@ -41,7 +41,7 @@
|
||||
<string name="cpu_jit_description">Uses the Just-in-Time (JIT) compiler for CPU emulation. When enabled, game performance will be significantly improved.</string>
|
||||
<string name="init_clock">System clock type</string>
|
||||
<string name="init_clock_description">Set the emulated 3DS clock to either reflect that of your device or start at a simulated date and time.</string>
|
||||
<string name="design">Design</string>
|
||||
<string name="design">Change Theme (Light, Dark)</string>
|
||||
|
||||
<!-- System settings strings -->
|
||||
<string name="init_time">System clock starting time override</string>
|
||||
@ -71,6 +71,13 @@
|
||||
<string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string>
|
||||
<string name="debug_warning">Warning: Modifying these settings will slow emulation</string>
|
||||
|
||||
<!-- Premium strings -->
|
||||
<string name="premium_text">Premium</string>
|
||||
<string name="premium_settings_upsell">Upgrade to Premium and support Citra!</string>
|
||||
<string name="premium_settings_upsell_description">With Premium, you will support the developers to continue improving Citra, and gain access to these exclusive features!</string>
|
||||
<string name="premium_settings_welcome">Welcome to Premium.</string>
|
||||
<string name="premium_settings_welcome_description">Thank you for your support!</string>
|
||||
|
||||
<!-- Audio settings strings -->
|
||||
<string name="audio_stretch">Enable audio stretching</string>
|
||||
<string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user