purchases) {
112 | // Update products state according to the user purchases.
113 | }
114 |
115 | @Override
116 | public void onConsumeResponse(@NonNull BillingResult billingResult,
117 | @NonNull String purchaseToken) { }
118 |
119 | @Override
120 | public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
121 | // Automatically acknowledge purchases if required.
122 | DynamicBilling.getInstance().acknowledgePurchases(billingResult, purchases);
123 | }
124 | }
125 | ```
126 |
127 | ### Sponsor
128 |
129 | Please become a [sponsor][sponsor] to get a detailed guide and priority support.
130 |
131 | ### Dependency
132 |
133 | It depends on the [dynamic-utils][dynamic-utils] to perform various internal operations.
134 | So, its functions can also be used to perform other useful operations.
135 |
136 | ---
137 |
138 | ## Author
139 |
140 | Pranav Pandey
141 |
142 | [](https://github.com/pranavpandey)
143 | [](https://twitter.com/intent/follow?screen_name=pranavpandeydev)
144 | [](https://paypal.me/pranavpandeydev)
145 |
146 | ---
147 |
148 | ## License
149 |
150 | Copyright 2022-2024 Pranav Pandey
151 |
152 | Licensed under the Apache License, Version 2.0 (the "License");
153 | you may not use this file except in compliance with the License.
154 | You may obtain a copy of the License at
155 |
156 | http://www.apache.org/licenses/LICENSE-2.0
157 |
158 | Unless required by applicable law or agreed to in writing, software
159 | distributed under the License is distributed on an "AS IS" BASIS,
160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
161 | See the License for the specific language governing permissions and
162 | limitations under the License.
163 |
164 |
165 | [androidx]: https://developer.android.com/jetpack/androidx
166 | [androidx-migrate]: https://developer.android.com/jetpack/androidx/migrate
167 | [documentation]: https://pranavpandey.github.io/dynamic-billing
168 | [google play billing]: https://developer.android.com/google/play/billing/integrate
169 | [sponsor]: https://github.com/sponsors/pranavpandey
170 | [dynamic-utils]: https://github.com/pranavpandey/dynamic-utils
171 | [dynamic-support]: https://github.com/pranavpandey/dynamic-support
172 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2025 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | buildscript {
18 | ext.versions = [
19 | 'compileSdk': 35,
20 | 'minSdk' : 21,
21 | 'targetSdk' : 35,
22 | 'buildTools': '35.0.0',
23 | 'billing' : '6.2.1',
24 | 'dynamic' : '4.6.1',
25 | 'kotlin' : '1.9.24',
26 | 'support' : '6.4.0'
27 | ]
28 |
29 | repositories {
30 | mavenCentral()
31 | google()
32 | }
33 |
34 | dependencies {
35 | classpath 'com.android.tools.build:gradle:8.7.3'
36 | }
37 | }
38 |
39 | plugins {
40 | id("io.github.gradle-nexus.publish-plugin") version "2.0.0"
41 | }
42 |
43 | allprojects {
44 | repositories {
45 | mavenCentral()
46 | google()
47 | }
48 | }
49 |
50 | tasks.register('clean', Delete) {
51 | delete rootProject.layout.buildDirectory
52 | }
53 |
54 | ext {
55 | projectName = 'dynamic-billing'
56 | projectDesc = 'A library to implement Google Play in-app products and subscriptions ' +
57 | 'on Android.'
58 | versionDesc = 'A library to implement Google Play in-app products and subscriptions ' +
59 | 'on Android 4.1 (API 16) and above.'
60 | referenceTitle = 'Dynamic Billing API reference'
61 |
62 | siteUrl = 'https://github.com/pranavpandey/dynamic-billing'
63 | gitUrl = 'https://github.com/pranavpandey/dynamic-billing'
64 | issueUrl = 'https://github.com/pranavpandey/dynamic-billing/issues'
65 | githubUrl = 'pranavpandey/dynamic-billing'
66 |
67 | mavenRepo = 'android'
68 | mavenGroup = 'com.pranavpandey.android'
69 | mavenDir = 'com/pranavpandey/android'
70 | mavenArtifactId = 'dynamic-billing'
71 | mavenInceptionYear = '2022'
72 | mavenVersion = '1.2.0'
73 | mavenVersionCode = 7
74 |
75 | developerId = 'pranavpandey'
76 | developerName = 'Pranav Pandey'
77 | developerEmail = 'dynamic@pranavpandey.com'
78 |
79 | licenseName = 'The Apache Software License, Version 2.0'
80 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
81 | licenseDistribution = 'repo'
82 | allLicenses = ["Apache-2.0"]
83 |
84 | publication = 'local.properties'
85 |
86 | ext["signing.keyId"] = ''
87 | ext["signing.password"] = ''
88 | ext["signing.secretKeyRingFile"] = ''
89 |
90 | ossrhUsername = ''
91 | ossrhPassword = ''
92 | sonatypeStagingProfileId = ''
93 | }
94 |
95 | apply plugin: 'io.github.gradle-nexus.publish-plugin'
96 |
97 | File publish = project.rootProject.file("${publication}")
98 | if (publish.exists()) {
99 | Properties properties = new Properties()
100 | new FileInputStream(publish).withCloseable { is -> properties.load(is) }
101 | properties.each { name, value -> ext[name] = value }
102 | }
103 |
104 | nexusPublishing {
105 | repositories {
106 | sonatype {
107 | username = ossrhUsername
108 | password = ossrhPassword
109 | stagingProfileId = sonatypeStagingProfileId
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/dynamic-billing/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2024 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'com.android.library'
18 |
19 | android {
20 | compileSdkVersion versions.compileSdk
21 | buildToolsVersion versions.buildTools
22 | namespace 'com.pranavpandey.android.dynamic.billing'
23 |
24 | defaultConfig {
25 | minSdkVersion versions.minSdk
26 | targetSdkVersion versions.targetSdk
27 |
28 | vectorDrawables.useSupportLibrary = true
29 | }
30 |
31 | sourceSets {
32 | main.res.srcDirs 'res'
33 | }
34 |
35 | compileOptions {
36 | sourceCompatibility JavaVersion.VERSION_17
37 | targetCompatibility JavaVersion.VERSION_17
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:${versions.kotlin}"))
43 |
44 | api "com.pranavpandey.android:dynamic-utils:${versions.dynamic}"
45 | api "com.android.billingclient:billing:${versions.billing}"
46 | implementation "com.pranavpandey.android:dynamic-support:${versions.support}"
47 | }
48 |
49 | if (project.rootProject.file("${publication}").exists()) {
50 | apply from: 'maven.gradle'
51 | }
52 |
53 | tasks.register('generateJavadoc') {
54 | description "Generates Javadoc."
55 | }
56 |
57 | project.afterEvaluate {
58 | android.libraryVariants.configureEach { variant ->
59 | def task = project.tasks.create(
60 | "generate${variant.name.capitalize()}Javadoc", Javadoc) {
61 | title "${referenceTitle}${versionDesc}
${mavenVersion}
"
62 | description "Generates Javadoc for $variant.name."
63 | destinationDir = new File(destinationDir, variant.baseName)
64 |
65 | source = variant.sourceSets.collect {
66 | it.java.sourceFiles
67 | }.inject {
68 | m, i -> m + i
69 | }
70 | doFirst {
71 | classpath = project.files(variant.javaCompileProvider.get().classpath.files,
72 | project.android.getBootClasspath())
73 | }
74 |
75 | if (JavaVersion.current().isJava8Compatible()) {
76 | options.addStringOption('Xdoclint:none', '-quiet')
77 | }
78 |
79 | options.memberLevel = JavadocMemberLevel.PROTECTED
80 | exclude "**/R", "**/R.**", "**/R\$**", "**/BuildConfig*"
81 |
82 | options.windowTitle = "${referenceTitle}"
83 | options.links('http://docs.oracle.com/javase/8/docs/api',
84 | 'http://docs.oracle.com/javase/17/docs/api')
85 | options.links('https://developer.android.com/reference')
86 | options.linksOffline('https://developer.android.com/reference',
87 | 'https://developer.android.com/reference/androidx')
88 | options.linksOffline('https://developer.android.com/reference',
89 | 'https://developer.android.com/reference/com/google/android/material')
90 | options.links('https://pranavpandey.org/dynamic-utils')
91 | options.links('https://pranavpandey.org/dynamic-support')
92 |
93 | failOnError false
94 | }
95 |
96 | task.dependsOn "assemble${variant.name.capitalize()}"
97 | generateJavadoc.dependsOn task
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/dynamic-billing/maven.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2024 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'maven-publish'
18 | apply plugin: 'signing'
19 |
20 | group = mavenGroup
21 | version = mavenVersion
22 |
23 | // Android libraries
24 | if (project.hasProperty("android")) {
25 | tasks.register('sourcesJar', Jar) {
26 | archiveClassifier.set("sources")
27 | from android.sourceSets.main.java.srcDirs
28 | }
29 |
30 | tasks.register('javadoc', Javadoc) {
31 | dependsOn "generateReleaseRFile"
32 | title "${referenceTitle}${versionDesc}
${mavenVersion}
"
33 | failOnError = false
34 |
35 | source = android.sourceSets.main.java.sourceFiles
36 | doNotTrackState("Javadoc needs to be generated every time.")
37 |
38 | if (JavaVersion.current().isJava8Compatible()) {
39 | options.addStringOption('Xdoclint:none', '-quiet')
40 | }
41 |
42 | options.memberLevel = JavadocMemberLevel.PROTECTED
43 | exclude "**/R", "**/R.**", "**/R\$**", "**/BuildConfig*"
44 |
45 | options.windowTitle = "${referenceTitle}"
46 | options.links('http://docs.oracle.com/javase/8/docs/api',
47 | 'http://docs.oracle.com/javase/17/docs/api')
48 | options.links('https://developer.android.com/reference')
49 | options.linksOffline('https://developer.android.com/reference',
50 | 'https://developer.android.com/reference/androidx')
51 | options.linksOffline('https://developer.android.com/reference',
52 | 'https://developer.android.com/reference/com/google/android/material')
53 | options.links('https://pranavpandey.org/dynamic-utils')
54 | options.links('https://pranavpandey.org/dynamic-support')
55 | }
56 | } else { // Java libraries
57 | tasks.register('sourcesJar', Jar) {
58 | dependsOn classes
59 |
60 | archiveClassifier.set("sources")
61 | from sourceSets.main.allSource
62 | }
63 | }
64 |
65 | tasks.register('javadocJar', Jar) {
66 | dependsOn javadoc
67 |
68 | archiveClassifier.set("javadoc")
69 | from javadoc.destinationDir
70 | }
71 |
72 | artifacts {
73 | archives javadocJar
74 | archives sourcesJar
75 | }
76 |
77 | // Maven
78 | publishing {
79 | publications {
80 | library(MavenPublication) {
81 | groupId mavenGroup
82 | artifactId mavenArtifactId
83 | version mavenVersion
84 |
85 | artifact "$buildDir/outputs/aar/$mavenArtifactId-release.aar"
86 | artifact javadocJar
87 | artifact sourcesJar
88 |
89 | pom.withXml {
90 | // Project
91 | asNode().appendNode('name', projectName)
92 | asNode().appendNode('description', projectDesc)
93 | asNode().appendNode('url', siteUrl)
94 | asNode().appendNode('inceptionYear', mavenInceptionYear)
95 |
96 | // Licenses
97 | def license = asNode().appendNode('licenses').appendNode('license')
98 | license.appendNode('name', licenseName)
99 | license.appendNode('url', licenseUrl)
100 | license.appendNode('distribution', licenseDistribution)
101 |
102 | // Developers
103 | def developer = asNode().appendNode('developers').appendNode('developer')
104 | developer.appendNode('id', developerId)
105 | developer.appendNode('name', developerName)
106 | developer.appendNode('email', developerEmail)
107 |
108 | // SCM
109 | def scm = asNode().appendNode('scm')
110 | scm.appendNode('connection', "scm:git:${gitUrl}.git")
111 | scm.appendNode('developerConnection', gitUrl)
112 | scm.appendNode('url', siteUrl)
113 |
114 | // Dependencies
115 | def dependenciesNode = asNode()['dependencies'][0]
116 | if (dependenciesNode == null) {
117 | dependenciesNode = asNode().appendNode('dependencies')
118 | }
119 |
120 | // Add all that are 'compile' dependencies.
121 | configurations.api.allDependencies.each {
122 | def dependencyNode = dependenciesNode.appendNode('dependency')
123 | dependencyNode.appendNode('groupId', it.group)
124 | dependencyNode.appendNode('artifactId', it.name)
125 | dependencyNode.appendNode('version', it.version)
126 | }
127 | }
128 | }
129 | }
130 | }
131 |
132 | ext["signing.keyId"] = rootProject.ext["signing.keyId"]
133 | ext["signing.password"] = rootProject.ext["signing.password"]
134 | ext["signing.secretKeyRingFile"] = rootProject.ext["signing.secretKeyRingFile"]
135 |
136 | signing {
137 | sign publishing.publications
138 | }
139 |
140 | afterEvaluate { project ->
141 | // Fix javadoc generation.
142 | javadoc.classpath += files(android.libraryVariants.collect { variant ->
143 | variant.javaCompileProvider.get().classpath.files
144 | })
145 |
146 | def pomTask = "generatePomFileForLibraryPublication"
147 | def dependencies = [javadocJar, sourcesJar, assembleRelease, pomTask]
148 |
149 | // Convenience task to prepare everything we need for releases.
150 | tasks.register('prepareArtifacts') {
151 | dependsOn dependencies
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/DynamicBilling.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2025 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing;
18 |
19 | import android.annotation.SuppressLint;
20 | import android.app.Activity;
21 | import android.app.Application;
22 | import android.content.Context;
23 | import android.os.Handler;
24 | import android.os.Looper;
25 |
26 | import androidx.annotation.NonNull;
27 | import androidx.annotation.Nullable;
28 | import androidx.annotation.RestrictTo;
29 |
30 | import com.android.billingclient.api.AcknowledgePurchaseParams;
31 | import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
32 | import com.android.billingclient.api.BillingClient;
33 | import com.android.billingclient.api.BillingClientStateListener;
34 | import com.android.billingclient.api.BillingFlowParams;
35 | import com.android.billingclient.api.BillingResult;
36 | import com.android.billingclient.api.ConsumeParams;
37 | import com.android.billingclient.api.ConsumeResponseListener;
38 | import com.android.billingclient.api.ProductDetails;
39 | import com.android.billingclient.api.ProductDetailsResponseListener;
40 | import com.android.billingclient.api.Purchase;
41 | import com.android.billingclient.api.PurchasesResponseListener;
42 | import com.android.billingclient.api.PurchasesUpdatedListener;
43 | import com.android.billingclient.api.QueryProductDetailsParams;
44 | import com.android.billingclient.api.QueryPurchasesParams;
45 | import com.pranavpandey.android.dynamic.billing.listener.DynamicBillingListener;
46 | import com.pranavpandey.android.dynamic.billing.model.DynamicFeature;
47 | import com.pranavpandey.android.dynamic.util.DynamicLinkUtils;
48 |
49 | import java.util.ArrayList;
50 | import java.util.List;
51 |
52 | /**
53 | * Helper class to handle the billing related operations including subscriptions.
54 | * It must be initialized once before accessing its methods.
55 | */
56 | public class DynamicBilling {
57 |
58 | /**
59 | * URL constant to manage billing inside Google Play.
60 | */
61 | public static final String URL_GOOGLE_PLAY =
62 | "http://play.google.com/store/account/subscriptions?package=%1$s";
63 |
64 | /**
65 | * URL constant to manage subscription inside Google Play.
66 | */
67 | public static final String URL_GOOGLE_PLAY_SUB =
68 | "http://play.google.com/store/account/subscriptions?sku=%1$s&package=%2$s";
69 |
70 | /**
71 | * Singleton instance of {@link DynamicBilling}.
72 | */
73 | @SuppressLint("StaticFieldLeak")
74 | private static DynamicBilling sInstance;
75 |
76 | /**
77 | * Context to retrieve the resources.
78 | */
79 | private Context mContext;
80 |
81 | /**
82 | * The billing client.
83 | */
84 | private BillingClient mBillingClient;
85 |
86 | /**
87 | * Listener to listen the billing client state events.
88 | */
89 | private final BillingClientStateListener mBillingStateListener;
90 |
91 | /**
92 | * Listener to listen the purchase update events.
93 | */
94 | private final PurchasesUpdatedListener mPurchasesUpdatedListener;
95 |
96 | /**
97 | * Listener to listen the product details event.
98 | */
99 | private final ProductDetailsResponseListener mProductDetailsResponseListener;
100 |
101 | /**
102 | * Listener to listen the purchase response.
103 | */
104 | private final PurchasesResponseListener mPurchasesResponseListener;
105 |
106 | /**
107 | * Listener to listen the purchase consumption.
108 | */
109 | private final ConsumeResponseListener mConsumeResponseListener;
110 |
111 | /**
112 | * Listener to listen the purchase acknowledgement.
113 | */
114 | private final AcknowledgePurchaseResponseListener mAcknowledgePurchaseResponseListener;
115 |
116 | /**
117 | * List of listeners to receive billing callbacks.
118 | */
119 | private final List mBillingListeners;
120 |
121 | /**
122 | * The billing state result.
123 | */
124 | private BillingResult mBillingResult;
125 |
126 | /**
127 | * Main thread handler to publish results.
128 | */
129 | private final Handler mHandler;
130 |
131 | /**
132 | * Making default constructor private so that it cannot be initialized without a context.
133 | * Use {@link #initializeInstance(Context)} instead.
134 | */
135 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
136 | private DynamicBilling() {
137 | this.mBillingListeners = new ArrayList<>();
138 | this.mHandler = new Handler(Looper.getMainLooper());
139 |
140 | this.mBillingStateListener = new BillingClientStateListener() {
141 | @Override
142 | public void onBillingServiceDisconnected() {
143 | getHandler().post(new Runnable() {
144 | @Override
145 | public void run() {
146 | for (BillingClientStateListener listener : getPurchaseListeners()) {
147 | listener.onBillingServiceDisconnected();
148 | }
149 | }
150 | });
151 | }
152 |
153 | @Override
154 | public void onBillingSetupFinished(final @NonNull BillingResult billingResult) {
155 | mBillingResult = billingResult;
156 |
157 | if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
158 | onBillingServiceDisconnected();
159 |
160 | return;
161 | }
162 |
163 | getHandler().post(new Runnable() {
164 | @Override
165 | public void run() {
166 | for (BillingClientStateListener listener : getPurchaseListeners()) {
167 | listener.onBillingSetupFinished(billingResult);
168 | }
169 | }
170 | });
171 | }
172 | };
173 |
174 | this.mPurchasesUpdatedListener = new PurchasesUpdatedListener() {
175 | @Override
176 | public void onPurchasesUpdated(final @NonNull BillingResult billingResult,
177 | final @Nullable List purchases) {
178 | getHandler().post(new Runnable() {
179 | @Override
180 | public void run() {
181 | for (PurchasesUpdatedListener listener : getPurchaseListeners()) {
182 | listener.onPurchasesUpdated(billingResult, purchases);
183 | }
184 | }
185 | });
186 | }
187 | };
188 |
189 | this.mProductDetailsResponseListener = new ProductDetailsResponseListener() {
190 | @Override
191 | public void onProductDetailsResponse(final @NonNull BillingResult billingResult,
192 | final @NonNull List productDetails) {
193 | getHandler().post(new Runnable() {
194 | @Override
195 | public void run() {
196 | for (ProductDetailsResponseListener listener : getPurchaseListeners()) {
197 | listener.onProductDetailsResponse(billingResult, productDetails);
198 | }
199 | }
200 | });
201 | }
202 | };
203 |
204 | this.mPurchasesResponseListener = new PurchasesResponseListener() {
205 | @Override
206 | public void onQueryPurchasesResponse(final @NonNull BillingResult billingResult,
207 | final @NonNull List purchases) {
208 | getHandler().post(new Runnable() {
209 | @Override
210 | public void run() {
211 | for (PurchasesResponseListener listener : getPurchaseListeners()) {
212 | listener.onQueryPurchasesResponse(billingResult, purchases);
213 | }
214 | }
215 | });
216 | }
217 | };
218 |
219 | this.mConsumeResponseListener = new ConsumeResponseListener() {
220 | @Override
221 | public void onConsumeResponse(@NonNull BillingResult billingResult,
222 | @NonNull String purchaseToken) {
223 | getHandler().post(new Runnable() {
224 | @Override
225 | public void run() {
226 | for (ConsumeResponseListener listener : getPurchaseListeners()) {
227 | listener.onConsumeResponse(billingResult, purchaseToken);
228 | }
229 | }
230 | });
231 | }
232 | };
233 |
234 | this.mAcknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
235 | @Override
236 | public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
237 | getHandler().post(new Runnable() {
238 | @Override
239 | public void run() {
240 | for (AcknowledgePurchaseResponseListener listener
241 | : getPurchaseListeners()) {
242 | listener.onAcknowledgePurchaseResponse(billingResult);
243 | }
244 | }
245 | });
246 | }
247 | };
248 | }
249 |
250 | /**
251 | * Constructor to initialize an object of this class.
252 | *
253 | * @param context The context to be used.
254 | */
255 | private DynamicBilling(@NonNull Context context) {
256 | this();
257 |
258 | this.mContext = context;
259 |
260 | this.mBillingClient = BillingClient.newBuilder(getContext())
261 | .setListener(mPurchasesUpdatedListener)
262 | .enablePendingPurchases()
263 | .build();
264 |
265 | if (getContext() instanceof DynamicBillingListener) {
266 | addListener((DynamicBillingListener) getContext());
267 | }
268 |
269 | startConnection();
270 | }
271 |
272 | /**
273 | * Initialize the billing client when application starts.
274 | * Must be initialized once.
275 | *
276 | * @param context The context to retrieve resources.
277 | */
278 | public static synchronized void initializeInstance(@Nullable Context context) {
279 | if (context == null) {
280 | throw new NullPointerException("Context should not be null.");
281 | }
282 |
283 | if (sInstance == null) {
284 | sInstance = new DynamicBilling(!(context instanceof Application)
285 | ? context.getApplicationContext() : context);
286 | }
287 | }
288 |
289 | /**
290 | * Retrieves the singleton instance of {@link DynamicBilling}.
291 | *
Must be called before accessing the public methods.
292 | *
293 | * @return The singleton instance of {@link DynamicBilling}.
294 | */
295 | public static synchronized @NonNull DynamicBilling getInstance() {
296 | if (sInstance == null) {
297 | throw new IllegalStateException(DynamicBilling.class.getSimpleName() +
298 | " is not initialized, call initializeInstance(...) method first.");
299 | }
300 |
301 | return sInstance;
302 | }
303 |
304 | /**
305 | * Returns the context used by this instance.
306 | *
307 | * @return The context to retrieve the resources.
308 | */
309 | public @NonNull Context getContext() {
310 | return mContext;
311 | }
312 |
313 | /**
314 | * The initialized billing client.
315 | *
316 | * @return The initialized billing client.
317 | */
318 | public @NonNull BillingClient getBillingClient() {
319 | return mBillingClient;
320 | }
321 |
322 | /**
323 | * Returns the list of billing listeners handled by this handler.
324 | *
325 | * @return The list of billing listeners handled by this handler.
326 | */
327 | public @NonNull List getPurchaseListeners() {
328 | return mBillingListeners;
329 | }
330 |
331 | /**
332 | * Add a billing listener to receive the various callbacks.
333 | *
334 | * @param listener The billing listener to be added.
335 | *
336 | * @return The {@link DynamicBilling} object to allow for chaining of calls to set methods.
337 | *
338 | * @see DynamicBillingListener
339 | */
340 | public @NonNull DynamicBilling addListener(@Nullable DynamicBillingListener listener) {
341 | if (listener != null && !getPurchaseListeners().contains(listener)) {
342 | getPurchaseListeners().add(listener);
343 |
344 | if (isConnected()) {
345 | listener.onBillingSetupFinished(mBillingResult);
346 | } else {
347 | startConnection();
348 | }
349 | }
350 |
351 | return this;
352 | }
353 |
354 | /**
355 | * Remove a billing listener.
356 | *
357 | * @param listener The billing listener to be removed.
358 | *
359 | * @return The {@link DynamicBilling} object to allow for chaining of calls to set methods.
360 | *
361 | * @see DynamicBillingListener
362 | */
363 | public @NonNull DynamicBilling removeListener(@Nullable DynamicBillingListener listener) {
364 | getPurchaseListeners().remove(listener);
365 |
366 | return this;
367 | }
368 |
369 | /**
370 | * Try to start connection with the billing service.
371 | */
372 | public void startConnection() {
373 | if (mBillingResult != null && (mBillingResult.getResponseCode()
374 | == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
375 | || mBillingResult.getResponseCode()
376 | == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE)) {
377 | if (mBillingStateListener != null) {
378 | mBillingStateListener.onBillingServiceDisconnected();
379 | }
380 |
381 | return;
382 | }
383 |
384 | if (isInitialized()) {
385 | mBillingClient.startConnection(mBillingStateListener);
386 | }
387 | }
388 |
389 | /**
390 | * Query product details for the supplied params.
391 | *
392 | * @param params The params to query the product details.
393 | */
394 | public void queryProductDetailsAsync(@NonNull QueryProductDetailsParams params) {
395 | if (!isInitialized() || !isConnected()) {
396 | startConnection();
397 |
398 | return;
399 | }
400 |
401 | mBillingClient.queryProductDetailsAsync(params, mProductDetailsResponseListener);
402 | }
403 |
404 | /**
405 | * Query purchases for the supplied params.
406 | *
407 | * @param params The params to query the purchases.
408 | */
409 | public void queryPurchasesAsync(@NonNull QueryPurchasesParams params) {
410 | if (!isInitialized() || !isConnected()) {
411 | startConnection();
412 |
413 | return;
414 | }
415 |
416 | mBillingClient.queryPurchasesAsync(params, mPurchasesResponseListener);
417 | }
418 |
419 | /**
420 | * Try to consume the supplied purchase.
421 | *
422 | * @param billingResult The billing result to be used.
423 | * @param purchase The purchase to be consumed.
424 | */
425 | public void consumePurchase(@NonNull BillingResult billingResult,
426 | @Nullable Purchase purchase) {
427 | if (!isInitialized() || !isConnected()) {
428 | startConnection();
429 |
430 | return;
431 | }
432 |
433 | if (purchase != null && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
434 | mBillingClient.consumeAsync(ConsumeParams.newBuilder()
435 | .setPurchaseToken(purchase.getPurchaseToken()).build(),
436 | mConsumeResponseListener);
437 | }
438 | }
439 |
440 | /**
441 | * Try to consume the supplied purchases.
442 | *
443 | * @param billingResult The billing result to be used.
444 | * @param purchases The purchases to be acknowledged.
445 | */
446 | public void consumePurchases(@NonNull BillingResult billingResult,
447 | @NonNull List purchases) {
448 | if (!isInitialized() || !isConnected()) {
449 | startConnection();
450 |
451 | return;
452 | }
453 |
454 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
455 | for (Purchase purchase : purchases) {
456 | consumePurchase(billingResult, purchase);
457 | }
458 | }
459 | }
460 |
461 | /**
462 | * Try to acknowledge the supplied purchase.
463 | *
464 | * @param billingResult The billing result to be used.
465 | * @param purchase The purchase to be acknowledged.
466 | */
467 | public void acknowledgePurchase(@NonNull BillingResult billingResult,
468 | @Nullable Purchase purchase) {
469 | if (!isInitialized() || !isConnected()) {
470 | startConnection();
471 |
472 | return;
473 | }
474 |
475 | if (purchase != null && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
476 | if (!purchase.isAcknowledged()) {
477 | mBillingClient.acknowledgePurchase(AcknowledgePurchaseParams.newBuilder()
478 | .setPurchaseToken(purchase.getPurchaseToken()).build(),
479 | mAcknowledgePurchaseResponseListener);
480 | }
481 | }
482 | }
483 |
484 | /**
485 | * Try to acknowledge the supplied purchases.
486 | *
487 | * @param billingResult The billing result to be used.
488 | * @param purchases The purchases to be acknowledged.
489 | */
490 | public void acknowledgePurchases(@NonNull BillingResult billingResult,
491 | @Nullable List purchases) {
492 | if (!isInitialized() || !isConnected()) {
493 | startConnection();
494 |
495 | return;
496 | }
497 |
498 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
499 | && purchases != null) {
500 | for (Purchase purchase : purchases) {
501 | acknowledgePurchase(billingResult, purchase);
502 | }
503 | }
504 | }
505 |
506 | /**
507 | * Launch the billing flow for the supplied params.
508 | *
509 | * @param activity The activity to be used.
510 | * @param params The billing flow params to be used.
511 | */
512 | public void launchBillingFlow(@Nullable Activity activity, @NonNull BillingFlowParams params) {
513 | if (activity == null) {
514 | return;
515 | }
516 |
517 | if (!isInitialized() || !isConnected()) {
518 | startConnection();
519 |
520 | return;
521 | }
522 |
523 | mBillingClient.launchBillingFlow(activity, params);
524 | }
525 |
526 | /**
527 | * Returns the main thread handler to publish results.
528 | *
529 | * @return The main thread handler to publish results.
530 | */
531 | public @NonNull Handler getHandler() {
532 | return mHandler;
533 | }
534 |
535 | /**
536 | * Returns whether the billing client has been initialized.
537 | *
538 | * @return {@code true} if the billing client have been initialized.
539 | */
540 | public boolean isInitialized() {
541 | return mBillingStateListener != null && mPurchasesUpdatedListener != null
542 | && mProductDetailsResponseListener != null && mBillingClient != null;
543 | }
544 |
545 | /**
546 | * Returns whether the billing client is connected.
547 | *
548 | * @return {@code true} if the billing client is connected.
549 | */
550 | public boolean isConnected() {
551 | return isInitialized() && mBillingClient.getConnectionState()
552 | == BillingClient.ConnectionState.CONNECTED;
553 | }
554 |
555 | /**
556 | * Try to find the dynamic feature for the supplied id.
557 | *
558 | * @param id The feature id to be used.
559 | *
560 | * @return The dynamic feature for the supplied id if found,
561 | * otherwise {@link DynamicFeature#UNKNOWN} feature.
562 | */
563 | public @NonNull DynamicFeature getFeatureById(@NonNull String id) {
564 | for (DynamicBillingListener listener : getPurchaseListeners()) {
565 | if (listener instanceof DynamicFeature
566 | && id.equals(((DynamicFeature) listener).getId())) {
567 | return (DynamicFeature) listener;
568 | }
569 | }
570 |
571 | return new DynamicFeature();
572 | }
573 |
574 | /**
575 | * Try to launch manage account flow for Google Play.
576 | */
577 | public void manageGooglePlay() {
578 | DynamicLinkUtils.viewUrl(getContext(), String.format(
579 | URL_GOOGLE_PLAY, getContext().getPackageName()));
580 | }
581 |
582 | /**
583 | * Try to launch manage subscription flow for Google Play.
584 | *
585 | * @param productId The product id for the subscription.
586 | */
587 | public void manageGooglePlaySubscription(@Nullable String productId) {
588 | if (productId == null) {
589 | manageGooglePlay();
590 |
591 | return;
592 | }
593 |
594 | DynamicLinkUtils.viewUrl(getContext(), String.format(
595 | URL_GOOGLE_PLAY_SUB, productId, getContext().getPackageName()));
596 | }
597 | }
598 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/adapter/DynamicFeaturesAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.adapter;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.annotation.Nullable;
21 | import androidx.recyclerview.widget.RecyclerView;
22 |
23 | import com.pranavpandey.android.dynamic.billing.binder.DynamicFeatureBinder;
24 | import com.pranavpandey.android.dynamic.support.model.DynamicInfo;
25 | import com.pranavpandey.android.dynamic.support.recyclerview.adapter.factory.SimpleDataBinderAdapter;
26 |
27 | import java.util.List;
28 |
29 | /**
30 | * A {@link SimpleDataBinderAdapter} to handle the {@link DynamicFeatureBinder}.
31 | */
32 | public class DynamicFeaturesAdapter extends
33 | SimpleDataBinderAdapter, DynamicFeatureBinder> {
34 |
35 | /**
36 | * Constructor to initialize an object of this class.
37 | *
38 | * @param data The data for this adapter.
39 | */
40 | public DynamicFeaturesAdapter(@Nullable List data) {
41 | addDataBinder(new DynamicFeatureBinder(this));
42 | setData(data);
43 | }
44 |
45 | @Override
46 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
47 | if (getData() != null) {
48 | getDataBinder(getItemViewType(position)).setData(getData().get(position));
49 | }
50 | super.onBindViewHolder(holder, position);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/binder/DynamicFeatureBinder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.binder;
18 |
19 | import android.view.LayoutInflater;
20 | import android.view.ViewGroup;
21 |
22 | import androidx.annotation.NonNull;
23 |
24 | import com.pranavpandey.android.dynamic.billing.R;
25 | import com.pranavpandey.android.dynamic.billing.adapter.DynamicFeaturesAdapter;
26 | import com.pranavpandey.android.dynamic.support.Defaults;
27 | import com.pranavpandey.android.dynamic.support.Dynamic;
28 | import com.pranavpandey.android.dynamic.support.model.DynamicInfo;
29 | import com.pranavpandey.android.dynamic.support.recyclerview.binder.factory.InfoBigBinder;
30 | import com.pranavpandey.android.dynamic.support.recyclerview.binder.factory.InfoBinder;
31 |
32 | /**
33 | * An {@link InfoBigBinder} to bind the {@link DynamicInfo} inside a
34 | * {@link androidx.cardview.widget.CardView} that can be used with the
35 | * {@link DynamicFeaturesAdapter}.
36 | */
37 | public class DynamicFeatureBinder extends InfoBinder {
38 |
39 | /**
40 | * Constructor to initialize an object of this class.
41 | *
42 | * @param binderAdapter The dynamic features adapter for the recycler view.
43 | */
44 | public DynamicFeatureBinder(@NonNull DynamicFeaturesAdapter binderAdapter) {
45 | super(binderAdapter);
46 | }
47 |
48 | @Override
49 | public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
50 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
51 | R.layout.adb_layout_feature_card, parent, false));
52 | }
53 |
54 | @Override
55 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
56 | super.onBindViewHolder(holder, position);
57 |
58 | Dynamic.setColorType(holder.getDynamicInfo().getIconView(),
59 | Defaults.ADS_COLOR_TYPE_ICON);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/listener/DynamicBillingListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.listener;
18 |
19 | import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
20 | import com.android.billingclient.api.BillingClientStateListener;
21 | import com.android.billingclient.api.ConsumeResponseListener;
22 | import com.android.billingclient.api.ProductDetailsResponseListener;
23 | import com.android.billingclient.api.PurchasesResponseListener;
24 | import com.android.billingclient.api.PurchasesUpdatedListener;
25 |
26 | /**
27 | * An interface to listen the billing callbacks.
28 | *
29 | * @see BillingClientStateListener
30 | * @see PurchasesUpdatedListener
31 | * @see ProductDetailsResponseListener
32 | * @see PurchasesResponseListener
33 | * @see ConsumeResponseListener
34 | * @see AcknowledgePurchaseResponseListener
35 | */
36 | public interface DynamicBillingListener extends BillingClientStateListener,
37 | PurchasesUpdatedListener, ProductDetailsResponseListener, PurchasesResponseListener,
38 | ConsumeResponseListener, AcknowledgePurchaseResponseListener { }
39 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/model/DynamicFeature.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.model;
18 |
19 | import android.os.Parcel;
20 | import android.os.Parcelable;
21 |
22 | import androidx.annotation.DrawableRes;
23 | import androidx.annotation.NonNull;
24 | import androidx.annotation.Nullable;
25 | import androidx.annotation.StringRes;
26 |
27 | import com.android.billingclient.api.BillingClient;
28 | import com.android.billingclient.api.BillingResult;
29 | import com.android.billingclient.api.ProductDetails;
30 | import com.android.billingclient.api.Purchase;
31 | import com.pranavpandey.android.dynamic.billing.R;
32 | import com.pranavpandey.android.dynamic.billing.listener.DynamicBillingListener;
33 |
34 | import java.util.ArrayList;
35 | import java.util.List;
36 |
37 | /**
38 | * A class to represent the dynamic feature.
39 | */
40 | public class DynamicFeature implements Parcelable, DynamicBillingListener {
41 |
42 | /**
43 | * Constant id for the unknown feature.
44 | */
45 | public static final String UNKNOWN = "adb_feature_unknown";
46 |
47 | /**
48 | * Constant value for the unknown resource.
49 | */
50 | public static final int UNKNOWN_RES = -1;
51 |
52 | /**
53 | * Unique id of the feature.
54 | */
55 | private final String id;
56 |
57 | /**
58 | * Icon drawable resource used by this feature.
59 | */
60 | private final @DrawableRes int icon;
61 |
62 | /**
63 | * Title string resource used by this feature.
64 | */
65 | private final @StringRes int title;
66 |
67 | /**
68 | * Subtitle string resource used by this feature.
69 | */
70 | private final @StringRes int subtitle;
71 |
72 | /**
73 | * List of products to enable this feature.
74 | */
75 | private final List products;
76 |
77 | /**
78 | * {@code true} if this feature is enabled.
79 | */
80 | private boolean enabled;
81 |
82 | /**
83 | * Constructor to initialize an object of this class.
84 | */
85 | public DynamicFeature() {
86 | this(UNKNOWN, new ArrayList<>());
87 | }
88 |
89 | /**
90 | * Constructor to initialize an object of this class.
91 | *
92 | * @param id The unique id for this feature.
93 | */
94 | public DynamicFeature(@NonNull String id) {
95 | this(id, new ArrayList<>());
96 | }
97 |
98 | /**
99 | * Constructor to initialize an object of this class.
100 | *
101 | * @param id The unique id for this feature.
102 | * @param products The list of products to enable this feature.
103 | */
104 | public DynamicFeature(@NonNull String id, @NonNull List products) {
105 | this(id, products, false);
106 | }
107 |
108 | /**
109 | * Constructor to initialize an object of this class.
110 | *
111 | * @param id The unique id for this feature.
112 | * @param products The list of products to enable this feature.
113 | * @param enabled {@code true} if this feature is enabled.
114 | */
115 | public DynamicFeature(@NonNull String id,
116 | @NonNull List products, boolean enabled) {
117 | this(id, R.drawable.adb_ic_product, R.string.adb_product, UNKNOWN_RES, products, enabled);
118 | }
119 |
120 | /**
121 | * Constructor to initialize an object of this class.
122 | *
123 | * @param id The unique id for this feature.
124 | * @param icon The icon drawable resource for this feature.
125 | * @param title The title string resource for this feature.
126 | * @param subtitle The subtitle string resource for this feature.
127 | * @param products The list of products to enable this feature.
128 | */
129 | public DynamicFeature(@NonNull String id, @DrawableRes int icon, @StringRes int title,
130 | @StringRes int subtitle, @NonNull List products) {
131 | this(id, icon, title, subtitle, products, false);
132 | }
133 |
134 | /**
135 | * Constructor to initialize an object of this class.
136 | *
137 | * @param id The unique id for this feature.
138 | * @param icon The icon drawable resource for this feature.
139 | * @param title The title string resource for this feature.
140 | * @param subtitle The subtitle string resource for this feature.
141 | * @param products The list of products to enable this feature.
142 | * @param enabled {@code true} if this feature is enabled.
143 | */
144 | public DynamicFeature(@NonNull String id, @DrawableRes int icon, @StringRes int title,
145 | @StringRes int subtitle, @NonNull List products, boolean enabled) {
146 | this.id = id;
147 | this.icon = icon;
148 | this.title = title;
149 | this.subtitle = subtitle;
150 | this.products = products;
151 | this.enabled = enabled;
152 | }
153 |
154 | /**
155 | * Read an object of this class from the parcel.
156 | *
157 | * @param in The parcel to read the values.
158 | */
159 | @SuppressWarnings("unchecked")
160 | public DynamicFeature(@NonNull Parcel in) {
161 | this.id = in.readString();
162 | this.icon = in.readInt();
163 | this.title = in.readInt();
164 | this.subtitle = in.readInt();
165 | this.products = in.readArrayList(DynamicProduct.class.getClassLoader());
166 | this.enabled = in.readByte() != 0;
167 | }
168 |
169 | /**
170 | * Parcelable creator to create from parcel.
171 | */
172 | public static final Creator CREATOR =
173 | new Creator() {
174 | @Override
175 | public DynamicFeature createFromParcel(Parcel in) {
176 | return new DynamicFeature(in);
177 | }
178 |
179 | @Override
180 | public DynamicFeature[] newArray(int size) {
181 | return new DynamicFeature[size];
182 | }
183 | };
184 |
185 | @Override
186 | public int describeContents() {
187 | return hashCode();
188 | }
189 |
190 | @Override
191 | public void writeToParcel(Parcel dest, int flags) {
192 | dest.writeString(id);
193 | dest.writeInt(icon);
194 | dest.writeInt(title);
195 | dest.writeInt(subtitle);
196 | dest.writeList(products);
197 | dest.writeByte((byte) (enabled ? 1 : 0));
198 | }
199 |
200 | /**
201 | * Returns the unique id for this feature.
202 | *
203 | * @return The unique id for this feature.
204 | */
205 | public @NonNull String getId() {
206 | return id;
207 | }
208 |
209 | /**
210 | * Returns the icon drawable resource used by this feature.
211 | *
212 | * @return The icon drawable resource used by this feature.
213 | */
214 | public @DrawableRes int getIcon() {
215 | return icon;
216 | }
217 |
218 | /**
219 | * Returns the title string resource used by this feature.
220 | *
221 | * @return The title string resource used by this feature.
222 | */
223 | public @StringRes int getTitle() {
224 | return title;
225 | }
226 |
227 | /**
228 | * Returns the subtitle string resource used by this feature.
229 | *
230 | * @return The subtitle string resource used by this feature.
231 | */
232 | public @StringRes int getSubtitle() {
233 | return subtitle;
234 | }
235 |
236 | /**
237 | * The list of products to enable this feature.
238 | *
239 | * @return The list of products to enable this feature.
240 | */
241 | public @NonNull List getProducts() {
242 | return products;
243 | }
244 |
245 | /**
246 | * Returns whether this feature is enabled.
247 | *
248 | * @return {@code true} if this feature is enabled.
249 | */
250 | public boolean isEnabled() {
251 | return enabled;
252 | }
253 |
254 | /**
255 | * Sets whether this feature is enabled.
256 | *
257 | * @param enabled {@code true} to enable this feature.
258 | */
259 | public void setEnabled(boolean enabled) {
260 | this.enabled = enabled;
261 | }
262 |
263 | /**
264 | * This method will be called to verify the status for this feature.
265 | *
266 | * @param billingResult The billing result to be used.
267 | * @param purchases The purchases to be verified.
268 | */
269 | public void onVerifyStatus(@NonNull BillingResult billingResult,
270 | @Nullable List purchases) {
271 | if (purchases == null || getProducts().isEmpty()
272 | || billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) {
273 | return;
274 | }
275 |
276 | setEnabled(false);
277 |
278 | for (DynamicProduct product : getProducts()) {
279 | for (Purchase purchase : purchases) {
280 | if (purchase.getProducts().contains(product.getId())) {
281 | setEnabled(true);
282 |
283 | break;
284 | }
285 | }
286 | }
287 | }
288 |
289 | @Override
290 | public void onBillingServiceDisconnected() { }
291 |
292 | @Override
293 | public void onBillingSetupFinished(@NonNull BillingResult billingResult) { }
294 |
295 | @Override
296 | public void onPurchasesUpdated(@NonNull BillingResult billingResult,
297 | @Nullable List purchases) {
298 | onVerifyStatus(billingResult, purchases);
299 | }
300 |
301 | @Override
302 | public void onProductDetailsResponse(@NonNull BillingResult billingResult,
303 | @NonNull List productDetails) { }
304 |
305 | @Override
306 | public void onQueryPurchasesResponse(@NonNull BillingResult billingResult,
307 | @NonNull List purchases) {
308 | onVerifyStatus(billingResult, purchases);
309 | }
310 |
311 | @Override
312 | public void onConsumeResponse(@NonNull BillingResult billingResult,
313 | @NonNull String purchaseToken) { }
314 |
315 | @Override
316 | public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) { }
317 | }
318 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/model/DynamicProduct.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2025 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.model;
18 |
19 | import android.os.Parcel;
20 | import android.os.Parcelable;
21 |
22 | import androidx.annotation.NonNull;
23 |
24 | import com.android.billingclient.api.BillingClient;
25 |
26 | import java.util.regex.Pattern;
27 |
28 | /**
29 | * A class to represent the in-app product or subscription.
30 | */
31 | public class DynamicProduct implements Parcelable {
32 |
33 | /**
34 | * Unique id of the product.
35 | */
36 | private final String id;
37 |
38 | /**
39 | * Product type of this product.
40 | */
41 | private final @BillingClient.ProductType String type;
42 |
43 | /**
44 | * Interface to hold the period constants.
45 | */
46 | public @interface Period {
47 |
48 | /**
49 | * Constant for the daily period.
50 | */
51 | String DAY = "D";
52 |
53 | /**
54 | * Constant for the weekly period.
55 | */
56 | String WEEK = "W";
57 |
58 | /**
59 | * Constant for the monthly period.
60 | */
61 | String MONTH = "M";
62 |
63 | /**
64 | * Constant for the yearly period.
65 | */
66 | String YEAR = "Y";
67 | }
68 |
69 | /**
70 | * Interface to hold the pricing and period patterns.
71 | */
72 | public @interface Patterns {
73 |
74 | /**
75 | * Pattern to match a digit.
76 | */
77 | Pattern DIGIT = Pattern.compile(".*\\d.*");
78 |
79 | /**
80 | * Pattern to match a number.
81 | */
82 | Pattern NUMBER = Pattern.compile("[^0-9]");
83 | }
84 |
85 | /**
86 | * Constructor to initialize an object of this class.
87 | *
88 | * @param id The unique id for this product.
89 | * @param type The type for this product.
90 | */
91 | public DynamicProduct(@NonNull String id, @BillingClient.ProductType String type) {
92 | this.id = id;
93 | this.type = type;
94 | }
95 |
96 | /**
97 | * Read an object of this class from the parcel.
98 | *
99 | * @param in The parcel to read the values.
100 | */
101 | public DynamicProduct(@NonNull Parcel in) {
102 | this.id = in.readString();
103 | this.type = in.readString();
104 | }
105 |
106 | /**
107 | * Parcelable creator to create from parcel.
108 | */
109 | public static final Parcelable.Creator CREATOR =
110 | new Parcelable.Creator() {
111 | @Override
112 | public DynamicProduct createFromParcel(Parcel in) {
113 | return new DynamicProduct(in);
114 | }
115 |
116 | @Override
117 | public DynamicProduct[] newArray(int size) {
118 | return new DynamicProduct[size];
119 | }
120 | };
121 |
122 | @Override
123 | public int describeContents() {
124 | return hashCode();
125 | }
126 |
127 | @Override
128 | public void writeToParcel(Parcel dest, int flags) {
129 | dest.writeString(id);
130 | dest.writeString(type);
131 | }
132 |
133 | /**
134 | * Returns the unique id for this product.
135 | *
136 | * @return The unique id for this product.
137 | */
138 | public @NonNull String getId() {
139 | return id;
140 | }
141 |
142 | /**
143 | * Returns the type for this product.
144 | *
145 | * @return The type for this product.
146 | */
147 | public @BillingClient.ProductType String getType() {
148 | return type;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/util/DynamicBillingUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022-2025 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.util;
18 |
19 | import android.content.Context;
20 |
21 | import androidx.annotation.Nullable;
22 |
23 | import com.android.billingclient.api.ProductDetails;
24 | import com.pranavpandey.android.dynamic.billing.R;
25 | import com.pranavpandey.android.dynamic.billing.model.DynamicProduct;
26 |
27 | import java.util.List;
28 |
29 | /**
30 | * Helper class to perform billing related operations.
31 | */
32 | public class DynamicBillingUtils {
33 |
34 | /**
35 | * Returns the user friendly string to display the trial information.
36 | *
37 | * @param context The context to retrieve resources.
38 | * @param period The ISO 8601 formatted pricing phase period.
39 | *
40 | * @return The user friendly string to display the trial information.
41 | */
42 | public static @Nullable String getStringForFreeTrial(
43 | @Nullable Context context, @Nullable String period) {
44 | if (context == null || period == null) {
45 | return null;
46 | }
47 |
48 | int periodCount = Integer.parseInt(period.replaceAll(
49 | DynamicProduct.Patterns.NUMBER.pattern(), ""));
50 |
51 | if (period.contains(DynamicProduct.Period.DAY)) {
52 | return String.format(context.getString(
53 | R.string.adb_offer_free_trial_day), periodCount);
54 | } else if (period.contains(DynamicProduct.Period.WEEK)) {
55 | return String.format(context.getString(
56 | R.string.adb_offer_free_trial_week), periodCount);
57 | } else if (period.contains(DynamicProduct.Period.MONTH)) {
58 | return String.format(context.getString(
59 | R.string.adb_offer_free_trial_month), periodCount);
60 | } else if (period.contains(DynamicProduct.Period.YEAR)) {
61 | return String.format(context.getString(
62 | R.string.adb_offer_free_trial_year), periodCount);
63 | }
64 |
65 | return period;
66 | }
67 |
68 | /**
69 | * Returns the user friendly string to display the first cycle information.
70 | *
71 | * @param context The context to retrieve resources.
72 | * @param formattedPrice The formatted price along with the currency symbol.
73 | * @param period The ISO 8601 formatted pricing phase period.
74 | *
75 | * @return The user friendly string to display the first cycle information.
76 | */
77 | public static @Nullable String getStringForFirstCycle(@Nullable Context context,
78 | @Nullable String formattedPrice, @Nullable String period) {
79 | if (context == null || period == null) {
80 | return null;
81 | }
82 |
83 | int periodCount = Integer.parseInt(period.replaceAll(
84 | DynamicProduct.Patterns.NUMBER.pattern(), ""));
85 | String formattedPeriod = period;
86 |
87 | if (period.contains(DynamicProduct.Period.DAY)) {
88 | formattedPeriod = periodCount > 1 ? String.format(
89 | context.getString(R.string.adb_offer_first_days), periodCount)
90 | : context.getString(R.string.adb_offer_first_day);
91 | } else if (period.contains(DynamicProduct.Period.WEEK)) {
92 | formattedPeriod = periodCount > 1 ? String.format(
93 | context.getString(R.string.adb_offer_first_weeks), periodCount)
94 | : context.getString(R.string.adb_offer_first_week);
95 | } else if (period.contains(DynamicProduct.Period.MONTH)) {
96 | formattedPeriod = periodCount > 1 ? String.format(
97 | context.getString(R.string.adb_offer_first_months), periodCount)
98 | : context.getString(R.string.adb_offer_first_month);
99 | } else if (period.contains(DynamicProduct.Period.YEAR)) {
100 | formattedPeriod = periodCount > 1 ? String.format(
101 | context.getString(R.string.adb_offer_first_years), periodCount)
102 | : context.getString(R.string.adb_offer_first_year);
103 | }
104 |
105 | return String.format(context.getString(R.string.ads_format_blank_space),
106 | formattedPrice, formattedPeriod);
107 | }
108 |
109 | /**
110 | * Returns the user friendly string to display the base cycle information.
111 | *
112 | * @param context The context to retrieve resources.
113 | * @param formattedPrice The formatted price along with the currency symbol.
114 | * @param period The ISO 8601 formatted pricing phase period.
115 | *
116 | * @return The user friendly string to display the base cycle information.
117 | */
118 | public static @Nullable String getStringForBaseCycle(@Nullable Context context,
119 | @Nullable String formattedPrice, @Nullable String period) {
120 | if (context == null || period == null) {
121 | return null;
122 | }
123 |
124 | int periodCount = Integer.parseInt(period.replaceAll(
125 | DynamicProduct.Patterns.NUMBER.pattern(), ""));
126 | String formattedPeriod = period;
127 |
128 | if (period.contains(DynamicProduct.Period.DAY)) {
129 | formattedPeriod = periodCount > 1 ? String.format(
130 | context.getString(R.string.adb_price_days), periodCount)
131 | : context.getString(R.string.adb_price_day);
132 | } else if (period.contains(DynamicProduct.Period.WEEK)) {
133 | formattedPeriod = periodCount > 1 ? String.format(
134 | context.getString(R.string.adb_price_weeks), periodCount)
135 | : context.getString(R.string.adb_price_week);
136 | } else if (period.contains(DynamicProduct.Period.MONTH)) {
137 | formattedPeriod = periodCount > 1 ? String.format(
138 | context.getString(R.string.adb_price_months), periodCount)
139 | : context.getString(R.string.adb_price_month);
140 | } else if (period.contains(DynamicProduct.Period.YEAR)) {
141 | formattedPeriod = periodCount > 1 ? String.format(
142 | context.getString(R.string.adb_price_years), periodCount)
143 | : context.getString(R.string.adb_price_year);
144 | }
145 |
146 | return String.format(context.getString(R.string.ads_format_blank_space),
147 | formattedPrice, formattedPeriod);
148 | }
149 |
150 | /**
151 | * Returns a string based on the formatted price, period and cycle count.
152 | *
153 | * @param context The context to retrieve resources.
154 | * @param formattedPrice The formatted price along with the currency symbol.
155 | * @param period The ISO 8601 formatted pricing phase period.
156 | * @param cycleCount The period cycle count.
157 | *
158 | * @return The string based on the formatted price, period and cycle count.
159 | */
160 | public static @Nullable String getStringForPeriod(@Nullable Context context,
161 | @Nullable String formattedPrice, @Nullable String period, int cycleCount) {
162 | if (context == null || formattedPrice == null || period == null) {
163 | return null;
164 | }
165 |
166 | if (DynamicProduct.Patterns.DIGIT.matcher(formattedPrice).matches()) {
167 | if (cycleCount == 1) {
168 | return getStringForFirstCycle(
169 | context, formattedPrice, period);
170 | } else {
171 | return getStringForBaseCycle(
172 | context, formattedPrice, period);
173 | }
174 | } else {
175 | return getStringForFreeTrial(context, period);
176 | }
177 | }
178 |
179 | /**
180 | * Returns a string based on the offer pricing phase(s).
181 | *
182 | * @param context The context to retrieve resources.
183 | * @param pricingPhases The pricing phases.
184 | * @param withBase {@code true} to include base pricing phase.
185 | *
186 | * @return A string based on the offer pricing phase(s).
187 | *
188 | * @see #getStringForPeriod(Context, String, String, int)
189 | */
190 | public static @Nullable String getPricingPhasesDetails(@Nullable Context context,
191 | @Nullable List pricingPhases, boolean withBase) {
192 | if (context == null || pricingPhases == null) {
193 | return null;
194 | }
195 |
196 | StringBuilder offerDetailsBuilder = new StringBuilder();
197 | for (ProductDetails.PricingPhase offerPricingPhase : pricingPhases) {
198 | if (!withBase && offerPricingPhase.getBillingCycleCount() <= 0) {
199 | continue;
200 | }
201 |
202 | String pricingPhaseInfo = getStringForPeriod(context,
203 | offerPricingPhase.getFormattedPrice(), offerPricingPhase.getBillingPeriod(),
204 | offerPricingPhase.getBillingCycleCount());
205 |
206 | if (offerDetailsBuilder.length() > 0) {
207 | offerDetailsBuilder.replace(0, offerDetailsBuilder.length(),
208 | String.format(context.getString(R.string.ads_format_next_line),
209 | offerDetailsBuilder, pricingPhaseInfo));
210 | } else {
211 | offerDetailsBuilder.append(pricingPhaseInfo);
212 | }
213 | }
214 |
215 | return offerDetailsBuilder.toString();
216 | }
217 |
218 | /**
219 | * Returns a string based on the offer pricing phase(s).
220 | *
221 | * @param context The context to retrieve resources.
222 | * @param offer The offer to retrieve the pricing phases.
223 | * @param withBase {@code true} to include base pricing phase.
224 | *
225 | * @return A string based on the offer pricing phase(s).
226 | *
227 | * @see #getPricingPhasesDetails(Context, List, boolean)
228 | */
229 | public static @Nullable String getOfferDetails(@Nullable Context context,
230 | @Nullable ProductDetails.SubscriptionOfferDetails offer, boolean withBase) {
231 | if (context == null || offer == null) {
232 | return null;
233 | }
234 |
235 | return getPricingPhasesDetails(context,
236 | offer.getPricingPhases().getPricingPhaseList(), withBase);
237 | }
238 |
239 | /**
240 | * Returns a string based on the base offer pricing.
241 | *
242 | * @param context The context to retrieve resources.
243 | * @param offer The offer to retrieve the pricing phases.
244 | *
245 | * @return A string based on the base offer pricing.
246 | *
247 | * @see #getPricingPhasesDetails(Context, List, boolean)
248 | */
249 | public static @Nullable String getOfferDetailsBase(@Nullable Context context,
250 | @Nullable ProductDetails.SubscriptionOfferDetails offer) {
251 | if (context == null || offer == null) {
252 | return null;
253 | }
254 |
255 | int size = offer.getPricingPhases().getPricingPhaseList().size();
256 | return getPricingPhasesDetails(context, offer.getPricingPhases()
257 | .getPricingPhaseList().subList(size - 1, size), true);
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/view/DynamicFeaturesView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.pranavpandey.android.dynamic.billing.view;
18 |
19 | import android.content.Context;
20 | import android.util.AttributeSet;
21 |
22 | import androidx.annotation.AttrRes;
23 | import androidx.annotation.LayoutRes;
24 | import androidx.annotation.NonNull;
25 | import androidx.annotation.Nullable;
26 | import androidx.recyclerview.widget.LinearLayoutManager;
27 | import androidx.recyclerview.widget.RecyclerView;
28 |
29 | import com.pranavpandey.android.dynamic.billing.R;
30 | import com.pranavpandey.android.dynamic.billing.adapter.DynamicFeaturesAdapter;
31 | import com.pranavpandey.android.dynamic.billing.model.DynamicFeature;
32 | import com.pranavpandey.android.dynamic.support.model.DynamicInfo;
33 | import com.pranavpandey.android.dynamic.support.recyclerview.DynamicRecyclerViewFrame;
34 | import com.pranavpandey.android.dynamic.support.recyclerview.DynamicRecyclerViewNested;
35 | import com.pranavpandey.android.dynamic.support.util.DynamicLayoutUtils;
36 | import com.pranavpandey.android.dynamic.support.util.DynamicResourceUtils;
37 |
38 | import java.util.ArrayList;
39 | import java.util.List;
40 |
41 | /**
42 | * A {@link DynamicRecyclerViewFrame} to display the list of {@link DynamicFeature}.
43 | */
44 | public abstract class DynamicFeaturesView extends DynamicRecyclerViewNested {
45 |
46 | public DynamicFeaturesView(@NonNull Context context) {
47 | this(context, null);
48 | }
49 |
50 | public DynamicFeaturesView(@NonNull Context context, @Nullable AttributeSet attrs) {
51 | super(context, attrs);
52 |
53 | setAdapter();
54 | }
55 |
56 | public DynamicFeaturesView(@NonNull Context context,
57 | @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
58 | super(context, attrs, defStyleAttr);
59 |
60 | setAdapter();
61 | }
62 |
63 | @Override
64 | protected @LayoutRes int getLayoutRes() {
65 | return R.layout.adb_features_view;
66 | }
67 |
68 | @Override
69 | public @Nullable RecyclerView.LayoutManager getRecyclerViewLayoutManager() {
70 | return DynamicLayoutUtils.getLinearLayoutManager(
71 | getContext(), LinearLayoutManager.HORIZONTAL);
72 | }
73 |
74 | /**
75 | * Returns a list of dynamic features to be displayed by this view.
76 | *
77 | * @return The list of dynamic features to be displayed by this view.
78 | */
79 | public abstract @NonNull List getFeatures();
80 |
81 | /**
82 | * Sets the adapter for his view.
83 | *
84 | * @return The {@link DynamicFeaturesView} object to allow for chaining of calls to
85 | * set methods.
86 | */
87 | public @NonNull DynamicFeaturesView setAdapter() {
88 | List features = new ArrayList<>();
89 | for (DynamicFeature feature : getFeatures()) {
90 | features.add(new DynamicInfo()
91 | .setIcon(DynamicResourceUtils.getDrawable(
92 | getContext(), feature.getIcon()))
93 | .setTitle(getContext().getString(feature.getTitle()))
94 | .setDescription(getContext().getString(feature.getSubtitle()))
95 | .setIconBig(DynamicResourceUtils.getDrawable(
96 | getContext(), R.drawable.adb_ic_feature)));
97 | }
98 |
99 | setAdapter(new DynamicFeaturesAdapter(features));
100 |
101 | return this;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_active.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_billing.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_feature.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_payments.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
28 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_product.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_restore.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
28 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/drawable/adb_ic_subscription.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/layout/adb_features_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
23 |
24 |
27 |
28 |
31 |
32 |
37 |
38 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/layout/adb_layout_feature_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/menu/adb_menu_manage.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
30 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Abrechnung
22 | Zahlungen
23 | Wiederherstellen
24 | Produkt
25 | Funktion
26 | Funktionen
27 | Abonnement
28 | Kaufen
29 | Kauf
30 | Käufe
31 | Abonnieren
32 | Aktiv
33 | Verwalten
34 | Abbrechen
35 | Ändern
36 | Migration
37 |
38 |
39 | Die Zahlung wird Ihrem Google Play-Konto am Ende der kostenlosen Testversion oder der Kaufbestätigung belastet, wenn Sie keine Testversion starten.
40 | Das Abonnement verlängert sich automatisch, wenn es nicht mindestens 24 Stunden vor dem Ende Ihrer kostenlosen Testversion oder des aktuellen Zeitraums gekündigt wird.
41 | Ihr Konto wird innerhalb von 24 Stunden vor dem Ende des aktuellen Zeitraums für die Verlängerung belastet.
42 | Sie können die Abonnements jederzeit in den Google Play-Einstellungen verwalten oder kündigen.
43 | Einkäufe erfolgreich wiederhergestellt.
44 | Käufe erfolgreich aktualisiert.
45 | Keine Einkäufe gefunden.
46 |
47 |
48 | pro Tag
49 | pro %1$d Tage
50 | pro Woche
51 | pro %1$d Wochen
52 | pro Monat
53 | pro %1$d Monate
54 | pro Jahr
55 | pro %1$d Jahre
56 |
57 |
58 | %1$d–tägige kostenlose Testversion
59 | %1$d–wöchige kostenlose Testversion
60 | %1$d–monatige kostenlose Testversion
61 | %1$d–Jahre kostenlose Testversion
62 | für den ersten Tag
63 | für die ersten %1$d Tage
64 | für die erste Woche
65 | für die ersten %1$d Wochen
66 | für den ersten Monat
67 | für die ersten %1$d Monate
68 | für das erste Jahr
69 | für die ersten %1$d Jahre
70 |
71 |
72 | Wiederholen
73 | Kann keine Verbindung zum Abrechnungsdienst herstellen.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Facturación
22 | Pagos
23 | Restaurar
24 | Producto
25 | Característica
26 | Características
27 | Suscripción
28 | Comprar
29 | Comprar
30 | Compras
31 | Suscribirse
32 | Activo
33 | Administrar
34 | Cancelar
35 | Cambiar
36 | Migrar
37 |
38 |
39 | El pago se cargará a su cuenta de Google Play al final de la prueba gratuita o la confirmación de la compra si no está iniciando una prueba.
40 | La suscripción se renovará automáticamente a menos que se cancele al menos 24 horas antes del final de su prueba gratuita o del período actual.
41 | Se le cobrará a su cuenta la renovación dentro de las 24 horas anteriores al final del período actual.
42 | Puede administrar o cancelar las suscripciones en cualquier momento en la configuración de Google Play.
43 | Compras restauradas con éxito.
44 | Compras actualizadas con éxito.
45 | No se encontraron compras.
46 |
47 |
48 | por dia
49 | por %1$d días
50 | por semana
51 | por %1$d semanas
52 | por mes
53 | por %1$d meses
54 | por año
55 | por %1$d años
56 |
57 |
58 | %1$d–día de prueba gratuita
59 | Prueba gratuita de %1$d–semana
60 | Prueba gratuita de %1$d–mes
61 | Prueba gratuita de %1$d–año
62 | para el primer dia
63 | durante los primeros %1$d días
64 | durante la primera semana
65 | durante las primeras %1$d semanas
66 | durante el primer mes
67 | durante los primeros %1$d meses
68 | durante el primer año
69 | durante los primeros %1$d años
70 |
71 |
72 | Reintentar
73 | No se pudo conectar al servicio de facturación.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Facturation
22 | Paiements
23 | Restaurer
24 | Produit
25 | Fonctionnalité
26 | Fonctionnalités
27 | Abonnement
28 | Acheter
29 | Achat
30 | Achats
31 | S\'abonner
32 | Actif
33 | Gérer
34 | Annuler
35 | Modifier
36 | Migrer
37 |
38 |
39 | Le paiement sera débité de votre compte Google Play à la fin de l\'essai gratuit ou de la confirmation d\'achat si vous ne démarrez pas d\'essai.
40 | L\'abonnement sera automatiquement renouvelé à moins qu\'il ne soit annulé au moins 24 heures avant la fin de votre essai gratuit ou de la période en cours.
41 | Votre compte sera facturé pour le renouvellement dans les 24 heures précédant la fin de la période en cours.
42 | Vous pouvez gérer ou annuler les abonnements à tout moment dans les paramètres de Google Play.
43 | Les achats ont été restaurés avec succès.
44 | Les achats ont été mis à jour avec succès.
45 | Aucun achat trouvé.
46 |
47 |
48 | par jour
49 | par %1$d jours
50 | par semaine
51 | par %1$d semaines
52 | par mois
53 | par %1$d mois
54 | par année
55 | par %1$d années
56 |
57 |
58 | Essai gratuit de %1$d\'jour
59 | Essai gratuit de %1$d–semaine
60 | Essai gratuit de %1$d\'mois
61 | Essai gratuit de %1$d\'année
62 | pour le premier jour
63 | pour les %1$d premiers jours
64 | pour la première semaine
65 | pour les %1$d premières semaines
66 | pour le premier mois
67 | pour les %1$d premiers mois
68 | pour la première année
69 | pour les %1$d premières années
70 |
71 |
72 | Réessayer
73 | Impossible de se connecter au service de facturation.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-hi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | बिलिंग
22 | भुगतान
23 | पुनर्स्थापित करें
24 | उत्पाद
25 | सुविधा
26 | विशेषताएं
27 | सदस्यता
28 | खरीदें
29 | खरीदें
30 | खरीदारी
31 | सदस्यता लें
32 | सक्रिय
33 | प्रबंधित करें
34 | रद्द करें
35 | बदलें
36 | माइग्रेट करें
37 |
38 |
39 | यदि आप परीक्षण शुरू नहीं कर रहे हैं तो नि: शुल्क परीक्षण या खरीद की पुष्टि के अंत में आपके Google Play खाते से भुगतान लिया जाएगा।
40 | सदस्यता स्वचालित रूप से नवीनीकृत हो जाएगी जब तक कि इसे आपके नि: शुल्क परीक्षण या वर्तमान अवधि के अंत से कम से कम 24 घंटे पहले रद्द नहीं किया जाता है।
41 | आपके खाते से वर्तमान अवधि की समाप्ति से 24 घंटे पहले नवीनीकरण के लिए शुल्क लिया जाएगा।
42 | आप Google Play सेटिंग में किसी भी समय सदस्यता को प्रबंधित या रद्द कर सकते हैं।
43 | खरीदारी सफलतापूर्वक बहाल हो गई।
44 | खरीदारी सफलतापूर्वक अपडेट की गई।
45 | कोई खरीदारी नहीं मिली।
46 |
47 |
48 | प्रति दिन
49 | प्रति %1$d दिन
50 | प्रति सप्ताह
51 | प्रति %1$d सप्ताह
52 | प्रति महीने
53 | प्रति %1$d माह
54 | प्रति वर्ष
55 | प्रति %1$d वर्ष
56 |
57 |
58 | %1$d–दिन का निःशुल्क परीक्षण
59 | %1$d–सप्ताह का निःशुल्क परीक्षण
60 | %1$d–माह निःशुल्क परीक्षण
61 | %1$d–वर्ष का निःशुल्क परीक्षण
62 | पहले दिन के लिए
63 | पहले %1$d दिनों के लिए
64 | पहले सप्ताह के लिए
65 | पहले %1$d सप्ताह के लिए
66 | पहले महीने के लिए
67 | पहले %1$d महीनों के लिए
68 | प्रथम वर्ष के लिए
69 | पहले %1$d वर्षों के लिए
70 |
71 |
72 | पुन: प्रयास करें
73 | बिलिंग सेवा से जुड़ने में असमर्थ।
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-in/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Penagihan
22 | Pembayaran
23 | Pulihkan
24 | Produk
25 | Fitur
26 | Fitur
27 | Langganan
28 | Beli
29 | Beli
30 | Pembelian
31 | Berlangganan
32 | Aktif
33 | Mengelola
34 | Batal
35 | Ubah
36 | Migrasi
37 |
38 |
39 | Pembayaran akan dibebankan ke akun Google Play Anda di akhir uji coba gratis atau konfirmasi pembelian jika Anda tidak memulai uji coba.
40 | Langganan akan diperpanjang secara otomatis kecuali dibatalkan setidaknya 24 jam sebelum akhir uji coba gratis atau periode berjalan Anda.
41 | Akun Anda akan dikenakan biaya untuk pembaruan dalam waktu 24 jam sebelum akhir periode berjalan.
42 | Anda dapat mengelola atau membatalkan langganan kapan saja di setelan Google Play.
43 | Pembelian berhasil dipulihkan.
44 | Purchases updated successfully.
45 | Tidak ada pembelian yang ditemukan.
46 |
47 |
48 | per hari
49 | per %1$d hari
50 | per minggu
51 | per %1$d minggu
52 | per bulan
53 | per %1$d bulan
54 | per tahun
55 | per %1$d tahun
56 |
57 |
58 | Uji coba gratis %1$d–hari
59 | Uji coba gratis %1$d–minggu
60 | Uji coba gratis %1$d–bulan
61 | Uji coba gratis %1$d–tahun
62 | untuk hari pertama
63 | untuk %1$d hari pertama
64 | untuk minggu pertama
65 | untuk %1$d minggu pertama
66 | untuk bulan pertama
67 | untuk %1$d bulan pertama
68 | untuk tahun pertama
69 | untuk %1$d tahun pertama
70 |
71 |
72 | Coba lagi
73 | Tidak dapat terhubung ke layanan penagihan.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Fatturazione
22 | Pagamenti
23 | Ripristina
24 | Prodotto
25 | Funzione
26 | Caratteristiche
27 | Abbonamento
28 | Compra
29 | Acquisto
30 | Acquisti
31 | Iscriviti
32 | Attivo
33 | Gestito
34 | Annulla
35 | Cambia
36 | Migra
37 |
38 |
39 | Il pagamento verrà addebitato sul tuo account Google Play al termine della prova gratuita o alla conferma dell\'acquisto se non stai iniziando una prova.
40 | L\'abbonamento si rinnoverà automaticamente a meno che non venga annullato almeno 24 ore prima della fine della prova gratuita o del periodo corrente.
41 | Il tuo account verrà addebitato per il rinnovo entro 24 ore prima della fine del periodo corrente.
42 | Puoi gestire o annullare gli abbonamenti in qualsiasi momento nelle impostazioni di Google Play.
43 | Acquisti ripristinati con successo.
44 | Purchases updated successfully.
45 | Nessun acquisto trovato.
46 |
47 |
48 | al giorno
49 | ogni %1$d giorni
50 | a settimana
51 | ogni %1$d settimane
52 | al mese
53 | ogni %1$d mesi
54 | all\'anno
55 | ogni %1$d anni
56 |
57 |
58 | Prova gratuita di %1$d–giorno
59 | Prova gratuita di %1$d–settimana
60 | Prova gratuita di %1$d–mese
61 | Prova gratuita di %1$d–anno
62 | per il primo giorno
63 | per i primi %1$d giorni
64 | per la prima settimana
65 | per le prime %1$d settimane
66 | per il primo mese
67 | per i primi %1$d mesi
68 | per il primo anno
69 | per i primi %1$d anni
70 |
71 |
72 | Riprova
73 | Impossibile connettersi al servizio di fatturazione.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-pt/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Faturamento
22 | Pagamentos
23 | Restaurar
24 | Produto
25 | Recurso
26 | Recursos
27 | Assinatura
28 | Comprar
29 | Compra
30 | Compras
31 | Inscrever-se
32 | Ativo
33 | Gerenciou
34 | Cancelar
35 | Alterar
36 | Migrar
37 |
38 |
39 | O pagamento será cobrado na sua conta do Google Play no final da avaliação gratuita ou na confirmação da compra, caso você não esteja iniciando uma avaliação.
40 | A assinatura será renovada automaticamente, a menos que seja cancelada pelo menos 24 horas antes do final de sua avaliação gratuita ou período atual.
41 | Sua conta será cobrada pela renovação dentro de 24 horas antes do final do período atual.
42 | Você pode gerenciar ou cancelar as assinaturas a qualquer momento nas configurações do Google Play.
43 | Compras restauradas com sucesso.
44 | Compras atualizadas com sucesso.
45 | Nenhuma compra encontrada.
46 |
47 |
48 | por dia
49 | por %1$d dias
50 | por semana
51 | por %1$d semanas
52 | por mês
53 | por %1$d meses
54 | por ano
55 | por %1$d anos
56 |
57 |
58 | %1$d–dia de avaliação gratuita
59 | %1$d–semana de avaliação gratuita
60 | %1$d–mês de avaliação gratuita
61 | Teste gratuito de %1$d–ano
62 | para o primeiro dia
63 | pelos primeiros %1$d dias
64 | para a primeira semana
65 | nas primeiras %1$d semanas
66 | para o primeiro mês
67 | pelos primeiros %1$d meses
68 | para o primeiro ano
69 | pelos primeiros %1$d anos
70 |
71 |
72 | Tentar novamente
73 | Não foi possível conectar ao serviço de faturamento.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Оплата
22 | Платежи
23 | Восстановить
24 | Продукт
25 | Функция
26 | Функции
27 | Подписка
28 | Купить
29 | Покупка
30 | Покупки
31 | Подписаться
32 | Активный
33 | Удалось
34 | Отменить
35 | Изменить
36 | Перенести
37 |
38 |
39 | Оплата будет снята с вашей учетной записи Google Play в конце бесплатного пробного периода или подтверждения покупки, если вы не начинаете пробный период.
40 | Подписка будет автоматически продлена, если она не будет отменена по крайней мере за 24 часа до окончания бесплатного пробного периода или текущего периода.
41 | С вашего счета будет взиматься плата за продление в течение 24 часов до окончания текущего периода.
42 | Вы можете управлять подписками или отменить их в любое время в настройках Google Play.
43 | Покупки успешно восстановлены.
44 | Покупки успешно обновлены.
45 | Покупки не найдены.
46 |
47 |
48 | в день
49 | за %1$d дней
50 | в неделю
51 | за %1$d недель
52 | помесячно
53 | за %1$d месяцев
54 | в год
55 | за %1$d лет
56 |
57 |
58 | %1$d–день бесплатной пробной версии
59 | %1$d–неделя бесплатной пробной версии
60 | %1$d–месяц бесплатная пробная версия
61 | %1$d–год бесплатная пробная версия
62 | в первый день
63 | за первые %1$d дней
64 | за первую неделю
65 | за первые %1$d недель
66 | за первый месяц
67 | за первые %1$d месяцев
68 | за первый год
69 | за первые %1$d лет
70 |
71 |
72 | Повторить попытку
73 | Не удалось подключиться к биллинговой службе.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Faturalandırma
22 | Ödemeler
23 | Geri yükle
24 | Ürün
25 | Özellik
26 | Özellikler
27 | Abonelik
28 | Satın al
29 | Satın alma
30 | Satın alınanlar
31 | Abone ol
32 | Etkin
33 | Yönetilen
34 | İptal
35 | Değiştir
36 | Taşıma
37 |
38 |
39 | Deneme sürümünü başlatmıyorsanız, ücretsiz denemenin sonunda veya satın alma onayının sonunda ödeme Google Play hesabınızdan tahsil edilecektir.
40 | Abonelik, ücretsiz deneme sürenizin veya mevcut sürenizin bitiminden en az 24 saat önce iptal edilmediği sürece otomatik olarak yenilenecektir.
41 | Cari dönemin bitiminden 24 saat önce hesabınızdan yenileme ücreti alınacaktır.
42 | Abonelikleri istediğiniz zaman Google Play ayarlarından yönetebilir veya iptal edebilirsiniz.
43 | Satın alınanlar başarıyla geri yüklendi.
44 | Satın alınanlar başarıyla güncellendi.
45 | Satın alma bulunamadı.
46 |
47 |
48 | günlük
49 | %1$d günde bir
50 | haftada
51 | %1$d haftada bir
52 | aylık
53 | %1$d ayda bir
54 | yıllık
55 | %1$d yılda bir
56 |
57 |
58 | %1$d–günlük ücretsiz deneme
59 | %1$d–haftalık ücretsiz deneme
60 | %1$d–aylık ücretsiz deneme
61 | %1$d–yıl ücretsiz deneme
62 | ilk gün için
63 | ilk %1$d gün için
64 | ilk hafta için
65 | ilk %1$d hafta için
66 | ilk ay için
67 | ilk %1$d ay için
68 | ilk yıl için
69 | ilk %1$d yıl için
70 |
71 |
72 | Yeniden dene
73 | Faturalandırma hizmetine bağlanılamıyor.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | 计费
22 | 付款
23 | 恢复
24 | 产品
25 | 功能
26 | 功能
27 | 订阅
28 | 购买
29 | 购买
30 | 购买
31 | 订阅
32 | 活跃
33 | 托管
34 | 取消
35 | 更改
36 | 迁移
37 |
38 |
39 | 如果您没有开始试用,则在免费试用结束或确认购买时,将向您的 Google Play 帐户收取费用。
40 | 订阅将自动续订,除非在免费试用期或当前期限结束前至少 24 小时取消。
41 | 您的帐户将在当前期限结束前 24 小时内收取续订费用。
42 | 您可以随时在 Google Play 设置中管理或取消订阅。
43 | 购买恢复成功。
44 | 购买更新成功。
45 | 未找到购买。
46 |
47 |
48 | 每天
49 | 每 %1$d 天
50 | 每周
51 | 每 %1$d 周
52 | 每月
53 | 每 %1$d 个月
54 | 每年
55 | 每 %1$d 年
56 |
57 |
58 | %1$d–天免费试用
59 | %1$d–周免费试用
60 | %1$d–月免费试用
61 | %1$d–年免费试用
62 | 第一天
63 | 前 %1$d 天
64 | 第一周
65 | 前 %1$d 周
66 | 第一个月
67 | 前 %1$d 个月
68 | 第一年
69 | 前 %1$d 年
70 |
71 |
72 | 重试
73 | 无法连接到计费服务。
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values-zh-rTW/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | 計費
22 | 付款
23 | 恢復
24 | 產品
25 | 功能
26 | 功能
27 | 訂閱
28 | 購買
29 | 購買
30 | 購買
31 | 訂閱
32 | 活躍
33 | 託管
34 | 取消
35 | 更改
36 | 遷移
37 |
38 |
39 | 如果您沒有開始試用,則在免費試用結束或確認購買時,將向您的 Google Play 帳戶收取費用。
40 | 訂閱將自動續訂,除非在免費試用期或當前期限結束前至少 24 小時取消。
41 | 您的帳戶將在當前期限結束前 24 小時內收取續訂費用。
42 | 您可以隨時在 Google Play 設置中管理或取消訂閱。
43 | 購買恢復成功。
44 | 購買更新成功。
45 | 未找到購買。
46 |
47 |
48 | 每天
49 | 每 %1$d 天
50 | 每週
51 | 每 %1$d 週
52 | 每月
53 | 每 %1$d 個月
54 | 每年
55 | 每 %1$d 年
56 |
57 |
58 | %1$d–天免費試用
59 | %1$d–週免費試用
60 | %1$d–月免費試用
61 | %1$d–年免費試用
62 | 第一天
63 | 前 %1$d 天
64 | 第一周
65 | 前 %1$d 週
66 | 第一個月
67 | 前 %1$d 個月
68 | 第一年
69 | 前 %1$d 年
70 |
71 |
72 | 重試
73 | 無法連接到計費服務。
74 |
75 |
76 |
--------------------------------------------------------------------------------
/dynamic-billing/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | Billing
22 | Payments
23 | Restore
24 | Product
25 | Feature
26 | Features
27 | Subscription
28 | Buy
29 | Purchase
30 | Purchases
31 | Subscribe
32 | Active
33 | Manage
34 | Cancel
35 | Change
36 | Migrate
37 |
38 |
39 | Payment will be charged to your Google Play account at the end of the free trial or confirmation of purchase if you are not starting a trial.
40 | Subscription will automatically renew unless it is canceled at least 24 hours before the end of your free trial or current period.
41 | Your account will be charged for renewal within 24 hours prior to the end of the current period.
42 | You can manage or cancel the subscriptions at any time in Google Play settings.
43 | Purchases restored successfully.
44 | Purchases updated successfully.
45 | No purchases found.
46 |
47 |
48 | per day
49 | per %1$d days
50 | per week
51 | per %1$d weeks
52 | per month
53 | per %1$d months
54 | per year
55 | per %1$d years
56 |
57 |
58 | %1$d–day free trial
59 | %1$d–week free trial
60 | %1$d–month free trial
61 | %1$d–year free trial
62 | for the first day
63 | for first %1$d days
64 | for the first week
65 | for first %1$d weeks
66 | for the first month
67 | for first %1$d months
68 | for the first year
69 | for first %1$d years
70 |
71 |
72 | Retry
73 | Unable to connect to the billing service.
74 |
75 |
76 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | android.nonTransitiveRClass=false
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranavpandey/dynamic-billing/2d34de76586102a423fe92e51f1450f509e9200d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/graphics/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranavpandey/dynamic-billing/2d34de76586102a423fe92e51f1450f509e9200d/graphics/icon.png
--------------------------------------------------------------------------------
/graphics/icon.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranavpandey/dynamic-billing/2d34de76586102a423fe92e51f1450f509e9200d/graphics/icon.psd
--------------------------------------------------------------------------------
/graphics/legacy/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranavpandey/dynamic-billing/2d34de76586102a423fe92e51f1450f509e9200d/graphics/legacy/icon.png
--------------------------------------------------------------------------------
/graphics/legacy/icon.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranavpandey/dynamic-billing/2d34de76586102a423fe92e51f1450f509e9200d/graphics/legacy/icon.psd
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 Pranav Pandey
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | include ':dynamic-billing'
18 |
--------------------------------------------------------------------------------