Skip to content

Commit 9a8dbe2

Browse files
first mockup of billing client emulation
mainly getting the classloader to work
1 parent 4869593 commit 9a8dbe2

File tree

12 files changed

+381
-0
lines changed

12 files changed

+381
-0
lines changed

LuckyXposed/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# LuckyXposed
2+
3+
Emulate in-app purchases.

LuckyXposed/build.gradle.kts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
plugins {
2+
alias(libs.plugins.buildlogic.android.application)
3+
alias(libs.plugins.buildlogic.kotlin.android)
4+
}
5+
6+
android {
7+
namespace = "com.programminghoch10.LuckyXposed"
8+
9+
defaultConfig {
10+
minSdk = 33
11+
targetSdk = 35
12+
}
13+
}
14+
15+
//noinspection UseTomlInstead
16+
dependencies {
17+
val billingVersion = "8.0.0"
18+
compileOnly("com.android.billingclient:billing:$billingVersion")
19+
//compileOnly("com.android.billingclient:billing-ktx:${billingVersion}")
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Emulate in-app purchases.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Emulate in-app purchases.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
LuckyXposed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest
3+
xmlns:android="http://schemas.android.com/apk/res/android">
4+
5+
<application android:label="LuckyXposed">
6+
<meta-data
7+
android:name="xposedmodule"
8+
android:value="true"
9+
/>
10+
<meta-data
11+
android:name="xposeddescription"
12+
android:value="Emulate successful in-app purchases."
13+
/>
14+
<meta-data
15+
android:name="xposedminversion"
16+
android:value="53"
17+
/>
18+
</application>
19+
20+
</manifest>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.programminghoch10.LuckyXposed.LuckyHook
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.programminghoch10.LuckyXposed;
2+
3+
import android.content.Context;
4+
import android.util.Log;
5+
6+
import com.android.billingclient.api.BillingClient;
7+
import com.android.billingclient.api.PurchasesUpdatedListener;
8+
import com.android.billingclient.api.UserChoiceBillingListener;
9+
10+
public class BillingClientBuilderStub {
11+
private static final String TAG = "Logger";
12+
13+
Context context;
14+
BillingClientBuilderStub(Context context) {
15+
this.context = context;
16+
}
17+
18+
public BillingClientBuilderStub enableAlternativeBillingOnly() {
19+
return this;
20+
}
21+
22+
public BillingClientBuilderStub enableExternalOffer() {
23+
return this;
24+
}
25+
26+
public BillingClientBuilderStub enablePendingPurchases() {
27+
return this;
28+
}
29+
30+
public BillingClientBuilderStub enableUserChoiceBilling(UserChoiceBillingListener userChoiceBillingListener) {
31+
return this;
32+
}
33+
34+
PurchasesUpdatedListener purchasesUpdatedListener;
35+
public BillingClientBuilderStub setListener(PurchasesUpdatedListener purchasesUpdatedListener) {
36+
this.purchasesUpdatedListener = purchasesUpdatedListener;
37+
return this;
38+
}
39+
40+
public BillingClient build() {
41+
Log.d(TAG, "build: build billingclient");
42+
if (purchasesUpdatedListener == null)
43+
throw new IllegalArgumentException("no purchasesUpdatedListener specified");
44+
return new BillingClientStub(context, purchasesUpdatedListener);
45+
}
46+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.programminghoch10.LuckyXposed;
2+
3+
import android.app.Activity;
4+
import android.content.Context;
5+
import android.os.Handler;
6+
import android.os.Looper;
7+
import android.util.Log;
8+
9+
import androidx.annotation.NonNull;
10+
11+
import java.lang.reflect.Proxy;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
15+
import com.android.billingclient.api.AcknowledgePurchaseParams;
16+
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
17+
import com.android.billingclient.api.AlternativeBillingOnlyAvailabilityListener;
18+
import com.android.billingclient.api.AlternativeBillingOnlyInformationDialogListener;
19+
import com.android.billingclient.api.AlternativeBillingOnlyReportingDetailsListener;
20+
import com.android.billingclient.api.BillingClient;
21+
import com.android.billingclient.api.BillingClientStateListener;
22+
import com.android.billingclient.api.BillingConfigResponseListener;
23+
import com.android.billingclient.api.BillingFlowParams;
24+
import com.android.billingclient.api.BillingResult;
25+
import com.android.billingclient.api.ConsumeParams;
26+
import com.android.billingclient.api.ConsumeResponseListener;
27+
import com.android.billingclient.api.ExternalOfferAvailabilityListener;
28+
import com.android.billingclient.api.ExternalOfferInformationDialogListener;
29+
import com.android.billingclient.api.ExternalOfferReportingDetailsListener;
30+
import com.android.billingclient.api.GetBillingConfigParams;
31+
import com.android.billingclient.api.InAppMessageParams;
32+
import com.android.billingclient.api.InAppMessageResponseListener;
33+
import com.android.billingclient.api.ProductDetails;
34+
import com.android.billingclient.api.ProductDetailsResponseListener;
35+
import com.android.billingclient.api.Purchase;
36+
import com.android.billingclient.api.PurchaseHistoryResponseListener;
37+
import com.android.billingclient.api.PurchasesResponseListener;
38+
import com.android.billingclient.api.PurchasesUpdatedListener;
39+
import com.android.billingclient.api.QueryProductDetailsParams;
40+
import com.android.billingclient.api.QueryPurchasesParams;
41+
42+
public class BillingClientStub extends BillingClient {
43+
private static final BillingResult BillingResultOK =
44+
BillingResult
45+
.newBuilder()
46+
.setResponseCode(BillingResponseCode.OK)
47+
.setDebugMessage("BillingClientStub BillingResultOK")
48+
.build();
49+
private static final String TAG = "Logger";
50+
private final Context context;
51+
private final PurchasesUpdatedListener purchasesUpdatedListener;
52+
53+
BillingClientStub(Context context, PurchasesUpdatedListener purchasesUpdatedListener) {
54+
super();
55+
this.context = context;
56+
this.purchasesUpdatedListener = purchasesUpdatedListener;
57+
Log.d(TAG, "BillingClientStub: construct stub");
58+
}
59+
60+
@Override
61+
public int getConnectionState() {
62+
Log.d(TAG, "getConnectionState: ");
63+
return ConnectionState.CONNECTED;
64+
}
65+
66+
@NonNull
67+
@Override
68+
public BillingResult isFeatureSupported(@NonNull String s) {
69+
Log.d(TAG, "isFeatureSupported: ");
70+
return BillingResultOK;
71+
}
72+
73+
@NonNull
74+
@Override
75+
public BillingResult launchBillingFlow(@NonNull Activity activity, @NonNull BillingFlowParams billingFlowParams) {
76+
Log.d(TAG, "launchBillingFlow: ");
77+
return BillingResultOK;
78+
}
79+
80+
@NonNull
81+
@Override
82+
public BillingResult showAlternativeBillingOnlyInformationDialog(
83+
@NonNull Activity activity,
84+
@NonNull AlternativeBillingOnlyInformationDialogListener alternativeBillingOnlyInformationDialogListener
85+
) {
86+
throw new IllegalStateException("not implemented");
87+
}
88+
89+
@NonNull
90+
@Override
91+
public BillingResult showExternalOfferInformationDialog(
92+
@NonNull Activity activity,
93+
@NonNull ExternalOfferInformationDialogListener externalOfferInformationDialogListener
94+
) {
95+
throw new IllegalStateException("not implemented");
96+
}
97+
98+
@NonNull
99+
@Override
100+
public BillingResult showInAppMessages(
101+
@NonNull Activity activity,
102+
@NonNull InAppMessageParams inAppMessageParams,
103+
@NonNull InAppMessageResponseListener inAppMessageResponseListener
104+
) {
105+
throw new IllegalStateException("not implemented");
106+
}
107+
108+
@Override
109+
public void acknowledgePurchase(
110+
@NonNull AcknowledgePurchaseParams acknowledgePurchaseParams,
111+
@NonNull AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener
112+
) {
113+
Log.d(TAG, "acknowledgePurchase: ");
114+
acknowledgePurchaseResponseListener.onAcknowledgePurchaseResponse(BillingResultOK);
115+
}
116+
117+
@Override
118+
public void consumeAsync(@NonNull ConsumeParams consumeParams, @NonNull ConsumeResponseListener consumeResponseListener) {
119+
throw new IllegalStateException("not implemented");
120+
}
121+
122+
@Override
123+
public void createAlternativeBillingOnlyReportingDetailsAsync(@NonNull AlternativeBillingOnlyReportingDetailsListener alternativeBillingOnlyReportingDetailsListener) {
124+
throw new IllegalStateException("not implemented");
125+
}
126+
127+
@Override
128+
public void createExternalOfferReportingDetailsAsync(@NonNull ExternalOfferReportingDetailsListener externalOfferReportingDetailsListener) {
129+
throw new IllegalStateException("not implemented");
130+
}
131+
132+
@Override
133+
public void endConnection() {
134+
Log.d(TAG, "endConnection: ");
135+
this.isReady = false;
136+
}
137+
138+
@Override
139+
public void getBillingConfigAsync(
140+
@NonNull GetBillingConfigParams getBillingConfigParams,
141+
@NonNull BillingConfigResponseListener billingConfigResponseListener
142+
) {
143+
throw new IllegalStateException("not implemented");
144+
}
145+
146+
@Override
147+
public void isAlternativeBillingOnlyAvailableAsync(@NonNull AlternativeBillingOnlyAvailabilityListener alternativeBillingOnlyAvailabilityListener) {
148+
throw new IllegalStateException("not implemented");
149+
}
150+
151+
@Override
152+
public void isExternalOfferAvailableAsync(@NonNull ExternalOfferAvailabilityListener externalOfferAvailabilityListener) {
153+
throw new IllegalStateException("not implemented");
154+
}
155+
156+
@Override
157+
public void queryProductDetailsAsync(
158+
@NonNull QueryProductDetailsParams queryProductDetailsParams,
159+
@NonNull ProductDetailsResponseListener productDetailsResponseListener
160+
) {
161+
throw new IllegalStateException("not implemented");
162+
}
163+
164+
@Override
165+
public void queryPurchasesAsync(
166+
@NonNull QueryPurchasesParams queryPurchasesParams,
167+
@NonNull PurchasesResponseListener purchasesResponseListener
168+
) {
169+
throw new IllegalStateException("not implemented");
170+
}
171+
172+
BillingClientStateListener billingClientStateListener = null;
173+
@Override
174+
public void startConnection(@NonNull BillingClientStateListener billingClientStateListener) {
175+
Log.d(TAG, "startConnection: ");
176+
this.billingClientStateListener = billingClientStateListener;
177+
this.isReady = true;
178+
billingClientStateListener.onBillingSetupFinished(BillingResultOK);
179+
}
180+
181+
boolean isReady = false;
182+
@Override
183+
public boolean isReady() {
184+
Log.d(TAG, "isReady: " + isReady);
185+
return isReady;
186+
}
187+
188+
public static BillingClientBuilderStub newBuilderStub(@NonNull Context context) {
189+
return new BillingClientBuilderStub(context);
190+
}
191+
192+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.programminghoch10.LuckyXposed
2+
3+
import kotlin.collections.map
4+
import android.content.Context
5+
import android.util.Log
6+
import com.android.billingclient.api.BillingClient
7+
import com.android.billingclient.api.PurchasesUpdatedListener
8+
import de.robv.android.xposed.IXposedHookLoadPackage
9+
import de.robv.android.xposed.XC_MethodHook
10+
import de.robv.android.xposed.XposedHelpers
11+
import de.robv.android.xposed.callbacks.XC_LoadPackage
12+
13+
14+
class LuckyHook : IXposedHookLoadPackage {
15+
16+
public val TAG = "Logger"
17+
val LogHook = object: XC_MethodHook() {
18+
override fun beforeHookedMethod(param: MethodHookParam) {
19+
Log.d(TAG, "beforeHookedMethod: ${param.method.name}")
20+
}
21+
override fun afterHookedMethod(param: MethodHookParam) {
22+
Log.d(TAG, "afterHookedMethod: ${param.method.name}")
23+
}
24+
}
25+
26+
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
27+
28+
val moduleClassLoader = this::class.java.classLoader!!
29+
val BootClassLoaderClass = XposedHelpers.findClass("java.lang.BootClassLoader", moduleClassLoader)
30+
fun isBootClassLoader(classLoader: ClassLoader):Boolean {
31+
return BootClassLoaderClass.isAssignableFrom(classLoader::class.java)
32+
}
33+
fun getClassLoaderParentList(classLoader: ClassLoader) : List<ClassLoader> {
34+
return generateSequence(classLoader, {it.parent}).toList()
35+
}
36+
fun getClassLoaderParentListWithoutBootClassLoader(classLoader: ClassLoader) : List<ClassLoader> {
37+
return getClassLoaderParentList(classLoader).filterNot(::isBootClassLoader)
38+
}
39+
if (!cartesianProduct(getClassLoaderParentListWithoutBootClassLoader(moduleClassLoader), getClassLoaderParentListWithoutBootClassLoader(lpparam.classLoader))
40+
.any { (first, second) ->
41+
first == second
42+
}) {
43+
// determine top level classloader before BootClassLoader
44+
val topClassLoader = getClassLoaderParentListWithoutBootClassLoader(moduleClassLoader).last()
45+
XposedHelpers.setObjectField(topClassLoader, "parent", lpparam.classLoader)
46+
}
47+
48+
/*XposedHelpers.findAndHookMethod(BillingClientClass, "newBuilder",
49+
Context::class.java,
50+
object: XC_MethodReplacement() {
51+
override fun replaceHookedMethod(param: MethodHookParam): BillingClientBuilderStub {
52+
val context : Context = param.args[0] as Context
53+
Log.d(TAG, "replaceHookedMethod: replace newBuilder with stub context=${context}")
54+
return BillingClientStub.newBuilderStub(context)
55+
}
56+
})*/
57+
58+
fun getFieldByType(obj: Any, clazz: Class<*>): Any? {
59+
Log.d(TAG, "getFieldByType: obj=${obj} clazz=${clazz}")
60+
Log.d(TAG, "getFieldByType: ${obj.javaClass.declaredFields.size}")
61+
Log.d(TAG, "getFieldByType: ${obj::class.java.declaredFields.map { it.type to it.type.isAssignableFrom(clazz)}.filter { it.second }}")
62+
return XposedHelpers.getObjectField(obj, obj::class.java.declaredFields.find { it.type.isAssignableFrom(clazz)}!!.name)
63+
}
64+
65+
XposedHelpers.findAndHookMethod(BillingClient.Builder::class.java, "build", object: XC_MethodHook() {
66+
override fun beforeHookedMethod(param: MethodHookParam) {
67+
val purchasesUpdatedListener = getFieldByType(param.thisObject, PurchasesUpdatedListener::class.java) as PurchasesUpdatedListener?
68+
if (purchasesUpdatedListener == null) {
69+
Log.w(TAG, "replaceHookedMethod: purchasesUpdatedListener is null, not hooking further")
70+
return
71+
}
72+
val context = getFieldByType(param.thisObject, Context::class.java) as Context?
73+
if (context == null) {
74+
Log.e(TAG, "replaceHookedMethod: context is null")
75+
return
76+
}
77+
Log.d(TAG, "replaceHookedMethod: purchasesUpdatedListener=${purchasesUpdatedListener}")
78+
//param.result = BillingClientStub(context, purchasesUpdatedListener)
79+
param.result = BillingClientStub.newBuilderStub(context).setListener(purchasesUpdatedListener).build()
80+
}
81+
})
82+
83+
}
84+
}

0 commit comments

Comments
 (0)