This post is part of AppLovin Nonconsensual Installs > Execution Path. See important disclosures.
The WebViewClient C4785e() constructor prepares the web view. Where a response includes references to resources in com.applovin.array.resources, or otherwise within the arrayList2 list, local resource interception substitutes resources from the APK’s Resources folder.
public C4785e(ArrayList arrayList, InterfaceC4868a interfaceC4868a, Resources resources) { AbstractC4226k.m6579e(arrayList, "xhrRequestHandlersManagers"); this.f14379b = C5552e1.m8618a(Integer.MAX_VALUE, 6, null); this.f14380c = AbstractC1469t3.m3132E("VISUAL_STATE_CALLBACK"); ArrayList arrayList2 = new ArrayList(); C4117d c4117d = new C4117d("https", "embedded.directdownload.arrayengine.com"); arrayList2.add(new C4870c(c4117d, interfaceC4868a)); arrayList2.add(new C5273p0(c4117d, arrayList)); arrayList2.add(new C5049c(c4117d)); arrayList2.add(new C4870c(new C4117d("com.applovin.array.resources", ""), new C4115b(resources))); this.f14381d = arrayList2; ...
The most important local resource is app\src\main\assets\directdownload-ui\assets\index-BFfWBgBF.js, a substantial block of minified JavaScript which is 13,069 lines long after pretty-printing.
Among other tasks, this file parses a server response to create what it calls a mergedConfig which determines what settings to use for the possible installation. These settings include “IsAutoInstall”, which is set based on what is received from the server, potentially supplemented by a default value from a local data structure called wt.
function Op(e, t) { //check whether isAutoInstallEnabled return e ? e.toLowerCase() === pr : t ? t.toLowerCase() === pr : wt.isAutoInstallEnabled } const ... s = t[Ct.IsAutoInstallEnabled], o = t[Ct.IsAutoInstallEnabledOld], ... return {... isAutoInstallEnabled: Op(s, o), ...
The default value is to enable AutoInstall:
const wt = { //default settings arrayPrivacyPolicyUrl: "https://www.applovin.com/array-privacy-policy/", arrayTermsOfServiceUrl: "https://www.applovin.com/array-terms/", autoInstallDelayMs: 5e3, isAutoInstallEnabled: !0, isBugsReportingEnabled: !1, isInstallingProgressEnabled: !1, isVideoSectionEnabled: !1, isVideoSectionExpanded: !1, isOneClickInstallOnEnabled: !0 };
If isAutoInstall is set to true, then the JavaScript installs the app immediately:
Wt(() => { (async () => { ... const R = We(Qc); if (!(!t.viewModel.isFirstLoad || !t.viewModel.isAutoInstall || R)) { if (t.viewModel.autoInstallDelayMs <= 0) { c(); return ...
The JavaScript function c()proceeds with installation, passing execution to installApp():
async function c() { //run the install
...
message: "App download and install start"
...
M = await Ge.installApp(t.viewModel.packageName, t.viewModel.versionCode, t.adToken)
The installApp() function relies on a constant specifying the endpoint destination:
me = (e => (... e.InstallApp = "install-app" ...
The installApp() function calls that endpoint:
async installApp(t, r, n) { //bridge back to Java code
pe.leaveBreadcrumb({message: `Native method "${me.InstallApp}" call`});
const a = (o=this.xhrUrls)==null ? void 0 : o[me.InstallApp];
const s = await this.nativeXhrClient.makeNativeXhrRequest({
xhrUrl: a,
body: Ia({packageName: t, versionCode: r, adToken: n})
});
return this.unwrapNativeAppResponse(s.dataText, me.InstallApp);
}
The function makeNativeXhrRequest wraps the custom httpClient.makeHttpRequest which in turn wraps the standard XMLHttpRequest, which finally calls the endpoint:
async makeNativeXhrRequest(t) { const {body: r, headers: n, httpMethod: a = nt.Post, queryParams: s, shouldStartSpan: o, spanAttributes: d, xhrUrl: f} = t; ... return await this.httpClient.makeHttpRequest(v); class Hh { makeHttpRequest(t) { ... return new Promise((o, d) => { const f = new XMLHttpRequest; ... f.send(s);
Installation On Close
The preceding section explores JavaScript logic under the isAutoInstall configuration, which causes an immediate installation. But adjacent JavaScript code offers two other ways installations can proceed automatically (and still without user consent despite, in these paths, an installation screen briefly presented to the user): What the code calls “Install On Close”, and installation after a brief countdown. Key lines from the JavaScript setup that considers these possibilities:
e.AutoInstallDelayMs="ui.dd.mp.install_countdown_ms" e.IsOneClickInstallOnCloseEnabledOld="ui.dd.mp.one_click_install_on_close" e.IsOneClickInstallOnCloseEnabled="ui.dd.mp.highintent_oc"
If isOneClickInstallOnCloseEnabled, the JavaScript installs the app when the user closes the ad, also using the c() function. First, JavaScript subscribes function C() to listen for events named “native-cross-button-custom-behavior”:
function A() { Bo.setDispatchEventBehavior() } Wt(() => { Bo.setChannel((...U) => Ge.setNativeCrossButtonBehavior(...U)); const M = Yp.subscribe(C), R = wi(A, 10), F = Ce.topOpened.subscribe(R); return () => (M(), F()) ...
As a result, C() runs when the user taps the x button to close the screen proposing to install an app.
Then C() forms variable F to indicate whether installs on close are disabled, and variable U to indicate whether the timer is running. If either is true, tapping X simply closes the screen (via the return line in red below). (Of course if the timer is running, installation will still proceed in due course, as detailed in the next section.) But if both are false (installs on close is enabled, and timer is not running), then the code proceeds with installation. In particular, C() then logs this event as “Installation on ‘x’ button click”, and executes function c() to proceed with installation.
function C(M) { if (!M) return; if (We(Ce.topOpened)) return void Ce.closeTop(); const F = We(Dr).isOneClickInstallOnCloseEnabled, U = We(f).isRunning; if (!F || !U) { ... return } pe.leaveBreadcrumb({ message: 'Installation on "X" button click', metadata: { isOneClickInstallOnCloseEnabled: F }, type: "manual" }), c(), ie.reportEvent(he.UiNativeCrossButtonClick, { isOneClickInstallOnCloseEnabled: F, isTimerActive: U, shouldStartAutoInstall: !0 }), Pn(100).then(() => { Ge.closeUiOnNativeCrossButtonClick() })
Installation via Countdown
Alternatively, the JavaScript sets up a timer called f to count down from the number of milliseconds specified in autoInstallDelayMs:
Wt(() => { (async () => { ... const R = We(Qc); if (!(!t.viewModel.isFirstLoad || !t.viewModel.isAutoInstall || R)) { if (t.viewModel.autoInstallDelayMs <= 0) { c(); return } w(o) === Xe.NotInstalled && (We(Dr).isOneClickInstallOnCloseEnabled && (J(d, !0), ie.reportEvent(he.SetInstallationOnDismissEnabled, { value: !0 }), Ge.setInstallationOnDismissEnabled(!0)), f.reset(), f.start(t.viewModel.autoInstallDelayMs)) } })() });
The default starting value of the timer is 5e3, i.e. 5×103=5000 milliseconds=5 seconds.
const wt = {
...
autoInstallDelayMs: 5e3, ...
The timer’s onExpire event is set to trigger installing the app, again via the c() function:
function av(e, t) { var N; be(t, !0); const [r, n] = st(), a = () => ke(Nr, "$installationStateControlledStore", r), s = () => ke(f, "$timer", r); let o = He(gt(((N = a()) == null ? void 0 : N.status) ?? null)), d = He(!1); const f = rv({ ... onExpire: () => { ie.reportEvent(he.AppAutoInstallTimerEnd), c() ...
As the timer counts down, it updates an on-screen label with the number of seconds left. First, the code creates a string Ku for the label template, with a placeholder {secondsCountdown} to be replaced by the current remaining time:
Ku = "Install in {secondsCountdown}s",
Then, a reactive state accessor r binds to the timer, running every 100ms:
const [r, n] = st(), a = () => ke(Nr, "$installationStateControlledStore", r), s = () => ke(f, "$timer", r);
...
tickIntervalMs: t = 100, ...
At the tickInterval, the reactive computation z() checks the currentMs left on the timer, storing this in variable i:
const ev = 1e3;
...
let i = z(() => s().isExpired || s().isAborted || !s().isRunning ? null : Math.ceil(s().currentMs / ev));
The reactive data exposure declares property secondsCountdown to be equal to i:
secondsCountdown: w(i),
The function w() evaluates the reactive computation and returns its current value, in turn updating the UI:
function w(e) {
...
return r && (a = e, zn(a) && zl(a)), Hn && En.has(e) ? En.get(e) : e.v
Then the reactive UI updates the displayed value, running the standard JavaScript replace method to find the placeholder {secondscountdown} and substitute the current value of secondsCountdown.
let c = z(() => !t.wasAutoInstallCancelled && typeof t.secondsCountdown == "number"),
h = z(() => t.installationStatus === Xe.Downloading || t.installationStatus === Xe.Installing),
v = z(() => t.installationStatus === Xe.Installed),
_ = z(() => {
if (w(c)) return a()[V.InstallIn].replace("{secondsCountdown}", `${t.secondsCountdown}`);
The net effect is a simple countdown timer, albeit with the unusual characteristic of showing labels like “Install in 3s” (with the letter “s” denoting seconds, and with no space between the number and the units). Notably, this “Install in 3s” format exactly matches the display one user preserved in a video, and another in a screenshot.
Execution continues in Java interceptors route the /install-app call to Tmobile’s InstallerHelper.