AppLovin Execution Path: T-Mobile InstallerHelper performs an install

This post is part of AppLovin Nonconsensual Installs > Execution Path. See important disclosures.

T-Mobile’s InstallerHelper sends execution to the separate APK com.tmobile.dm.cm:

public final InstallerHelperResult startInstall(String absPath, String packageName, boolean shortcut, int requestedScreen, int colX, int rowY, String triggerType, String className, String action, String extraData, String receiverPermission) throws RemoteException {
    AbstractC4226k.m6579e(absPath, "absPath");
    AbstractC4226k.m6579e(packageName, "packageName");
    if (!isReady()) {
        if (this.f19986b) {
            Log.e("CM_TMO_SDK", "Cannot start, service not ready");
        }
        return new InstallerHelperResult.Fail(ConstantsKt.ERROR_KEY_NOT_BOUND, null);
    }
    ...
    Message messageObtain = Message.obtain();
    messageObtain.what = 100;
    messageObtain.replyTo = this.f19993i;
    File file = new File(absPath);
    Bundle bundle = new Bundle();
    bundle.putString(EXTRA_LOCATION, Uri.fromFile(file).toString());
    bundle.putString(EXTRA_PACKAGE, packageName);
    bundle.putString("com.tmobile.dm.cm.extra.PACKAGE_NAME", this.f19985a.getPackageName());
    bundle.putBoolean(EXTRA_SHORTCUT, shortcut);
    ...
    messageObtain.setData(bundle);
    try {
        Messenger messenger = this.f19992h;
        if (messenger != null) {
            messenger.send(messageObtain);
        }
        return new InstallerHelperResult.Success(ConstantsKt.SUCCESS_KEY);

According to the InstallerHelper class definition, the constant messageObtain.what=100 denotes performing an installation:

public static final int REQUEST_INSTALL = 100;

T-Mobile’s com.tmobile.dm.cm has special permissions including the permission to install apps. Its manifest declares these permissions:

<uses-permission android:name="android.permission.INSTALL_PACKAGES"/>

T-Mobile’s message dispatcher m14262a() receives the request from InstallerHelper, classifies the message based on message.what message type, and routes the message accordingly:

public final void m14262a(Message message) { //message dispatcher
    ...
    int i8 = message.what;
    ...
    switch (i8) {
    case 100:    
        companion.mo19362i("doHandleMsg(): Install requested: %s", 100);
        synchronized (this.f33457b) {
            size3 = this.f33457b.size();
        }
        Bundle data3 = message.getData();
        String m14260j = m14260j(data3.getString(AppConstants.EXTRA_REQUESTER_PACKAGE), data3.getStringArray(AppConstants.EXTRA_SERVICE_REQUESTER_PACKAGE));
        String string = data3.getString("location");
        String string2 = data3.getString(AppConstants.EXTRA_PACKAGE);
        int i9 = data3.getInt(AppConstants.EXTRA_UID, 0);
        boolean z7 = data3.getBoolean(AppConstants.EXTRA_SIGNED, true);
        Intrinsics.checkNotNull(messenger);
        LocalInstallHandlerParams localInstallHandlerParams = new LocalInstallHandlerParams(messenger, string, string2, i9, z7, m14260j);
        localInstallHandlerParams.setAddShortcut(data3.getBoolean(AppConstants.EXTRA_SHORTCUT, false));
        localInstallHandlerParams.setRequestedScreen(data3.getInt(AppConstants.EXTRA_REQUEST_SCREEN, 0));
        localInstallHandlerParams.setColX(data3.getInt(AppConstants.EXTRA_COLX, 0));
        localInstallHandlerParams.setRowY(data3.getInt(AppConstants.EXTRA_ROWY, 0));
        localInstallHandlerParams.setTriggerType(data3.getString(AppConstants.EXTRA_TRIGGER_TYPE));
        localInstallHandlerParams.setClassName(data3.getString(AppConstants.EXTRA_CLASS_NAME));
        localInstallHandlerParams.setAction(data3.getString("action"));
        localInstallHandlerParams.setExtraData(data3.getString(AppConstants.EXTRA_DATA));
        localInstallHandlerParams.setReceiverPermission(data3.getString(AppConstants.EXTRA_BROADCAST_PERMISSION));
        synchronized (this.f33457b) {
            this.f33457b.add(size3, localInstallHandlerParams);
        }
        if (size3 == 0 && this.f33458c == null) {
            this.f33460e.sendEmptyMessage(102);
            return;
        }
        ...

The sendEmptyMessage(102) method sends execution to m14262a case 102, which again calls startInstall(), this time with case 102:

    case 102:
        companion.mo19362i("doHandleMsg(): Processing install: %s", 102);
        synchronized (this.f33457b) {
            size4 = this.f33457b.size();
        }
        if (size4 > 0) {
            synchronized (this.f33457b) {
                handlerParams = (HandlerParams) this.f33457b.remove(0);
                this.f33458c = handlerParams;
            }
            if (handlerParams != null) {
                if (handlerParams instanceof ModifyParams) {
                    ((ModifyParams) handlerParams).startModify(this);
                    return;
                } else if (handlerParams instanceof UninstallParams) {
                    ((UninstallParams) handlerParams).startUninstall(this);
                    return;
                } else {
                    handlerParams.startInstall(this);
                    return;
                }
            }
            return;
        }
        return;

Next handlerParams.startInstall() calls prepareInstall(), which in turn calls performInstallBundle():

public boolean startInstall(@NotNull ServiceHandler handler) {
    Intrinsics.checkNotNullParameter(handler, "handler");
    int prepareInstall = prepareInstall(handler);
    ...

public int prepareInstall(@NotNull ServiceHandler handler) {
    ...
    return performInstallBundle(handler.getMContext());
    }

Then performInstallBundle() passes execution to InstallParams method m14247a():

public final int performInstallBundle(@NotNull Context context) {
    …
    int m14247a = m14247a(context, "", arrayList, false);

Finally, m14247a tells the Android Package Manager to perform the install:

//pass install to Android Package Manager
public final int m14247a(Context context, String str, ArrayList arrayList, boolean z6) throws Throwable {
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
    ...
    PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(this.mInstallSessionMode);
    sessionParams.setAppLabel(getAppName(context));
    sessionParams.setAppPackageName(getCom.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.PACKAGE_NAME java.lang.String());
    ...
    setMInstallerSessionId$app_currentRelease(packageInstaller.createSession(sessionParams));
    ...
    this.f33245C = packageInstaller.openSession(getMInstallerSessionId());
    Iterator it = arrayList.iterator();
    Intrinsics.checkNotNullExpressionValue(it, "iterator(...)");
    int iM14248c2 = -999;
    while (it.hasNext()) {
        Object next = it.next();
        Intrinsics.checkNotNullExpressionValue(next, "next(...)");
        Timber.INSTANCE.getClass();
        iM14248c2 = m14248c((String) next);
        if (1 != iM14248c2) {
            break;
        }
    }
    ...
    Intent intent = new Intent(context, (Class<?>) InternalReceiver.class);
    intent.setAction(InternalReceiver.ACTION_INSTALLER_RESULT);
    PendingIntent broadcast = PendingIntent.getBroadcast(context, 0, intent, Build.VERSION.SDK_INT >= 31 ? 167772160 : 134217728);
    PackageInstaller.Session session = this.f33245C;
    if (session != null) {
        session.commit(broadcast.getIntentSender());
    }

AppHub uses a variety of installers with heightened privileges from manufacturer or carrier

The preceding section discusses AppHub passing execution to T-Mobile InstallerHelper. But consider devices that don’t have T-Mobile InstallerHelper. AppHub code shows AppLovin also using other install helpers from other manufacturers and carriers.

T-Mobile and Sprint

public final InstallerHelperResult bindToInstaller(boolean useForegroundServiceIfNeeded) {
...
PackageManager.PackageInfoFlags of;
PackageManager.PackageInfoFlags of2;
PackageManager.ResolveInfoFlags of3;
if (isReady()) {
if (this.f19986b) {
Log.e("CM_TMO_SDK", "Binding failed, service already bound");
}
return new InstallerHelperResult.Fail(ConstantsKt.ERROR_KEY_ALREADY_BOUND, null);
}
if (((Number) this.f19989e.getValue()).intValue() < 3000) {
return new InstallerHelperResult.Fail(ConstantsKt.ERROR_KEY_VERSION_NOT_SUPPORTED, null);
}
String str = "com.tmobile.dm.cm.extra.PACKAGE_NAME";
String str2 = "com.tmobile.dm.cm.extra.APP_LABEL";
if (AbstractC0945q.m2146P((String) this.f19987c.getValue(), "com.tmobile.dm.cm.permission.UPDATES_INSTALL", false)) {
intent = new Intent("com.tmobile.action.INSTALLER_SERVICE");
} else if (AbstractC0945q.m2146P((String) this.f19987c.getValue(), "com.tmobile.dm.cm.permission.TRUSTED_UPDATES_INSTALL", false)) {
intent = new Intent("com.tmobile.action.INSTALLER_TRUSTED_SERVICE");
} else {
str = "com.sprint.ce.updater.extra.PACKAGE_NAME";
str2 = "com.sprint.ce.updater.extra.APP_LABEL";
if (AbstractC0945q.m2146P((String) this.f19987c.getValue(), "com.sprint.permission.INSTALL_UPDATES", false)) {
intent = new Intent("com.sprint.action.INSTALLER_SERVICE");
} else {
if (!AbstractC0945q.m2146P((String) this.f19987c.getValue(), "com.sprint.ce.updater.permission.TRUSTED_INSTALL_UPDATES", false)) {
return new InstallerHelperResult.Fail(ConstantsKt.ERROR_KEY_PERMISSION_SECURITY_ISSUE, null);
}
intent = new Intent("com.sprint.action.INSTALLER_TRUSTED_SERVICE");
...

Samsung

public class SamsungBindInstallAgentService extends Hilt_SamsungBindInstallAgentService {
public static final String ACTION_INSTALL_PACKAGE_BY_SAMSUNG = "action_install_package_by_samsung";
private static final String INSTALL_AGENT_CLASS = "com.sec.android.app.samsungapps.api.InstallAgent";
private static final String INSTALL_AGENT_PACKAGE = "com.sec.android.app.samsungapps";
public static final String PARAM_DOWNLOAD_PACKAGE = "param_download_package";
ActiveDeliveryTrackerManager activeDeliveryTrackerManager;
AppDeliveryInfoDao appDeliveryInfoDao;
Executor deliveryCoordinatorExecutor;
private InterfaceC1402c installAgentAPI;
Logger logger;
SamsungErrorCodeManager samsungErrorCodeManager;
private final IBinder binder = new LocalBinder();
volatile boolean mBound = false;
volatile boolean isBinding = false;
IBinder samsungInstallerBinder = null;
String activeTargetPackageName = null;
private volatile ArrayDeque installTaskQueue = new ArrayDeque<>();
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
InterfaceC1402c c1400a;
SamsungBindInstallAgentService.this.logger.mo2231d( SamsungBindInstallAgentService.this.getClass().getSimpleName() + " : onServiceConnected() called with: className = [" + componentName + "], activeTargetPackageName = [" + SamsungBindInstallAgentService.this.activeTargetPackageName + "]");
SamsungBindInstallAgentService samsungBindInstallAgentService = SamsungBindInstallAgentService.this;
samsungBindInstallAgentService.samsungInstallerBinder = iBinder;
int i10 = AbstractBinderC1401b.f4081c;
if (iBinder == null) {
c1400a = null;
} else {
IInterface queryLocalInterface = iBinder.queryLocalInterface("com.sec.android.app.samsungapps.api.aidl.IInstallAgentAPI");
c1400a = (queryLocalInterface == null || !(queryLocalInterface instanceof InterfaceC1402c)) ? new C1400a(iBinder) : (InterfaceC1402c) queryLocalInterface;
}
samsungBindInstallAgentService.installAgentAPI = c1400a;
SamsungBindInstallAgentService.this.mBound = true;
SamsungBindInstallAgentService.this.isBinding = false;
if (TextUtils.isEmpty(SamsungBindInstallAgentService.this.activeTargetPackageName)) {
SamsungBindInstallAgentService.this.dequeueDownloadToken();
return;
}
try {
SamsungBindInstallAgentService.this.logger.mo2237i( SamsungBindInstallAgentService.this.getClass().getSimpleName() + " : resume install package after reconnected = " + SamsungBindInstallAgentService.this.activeTargetPackageName);
SamsungBindInstallAgentService samsungBindInstallAgentService2 = SamsungBindInstallAgentService.this;
samsungBindInstallAgentService2.startInstall( samsungBindInstallAgentService2.activeTargetPackageName);
} catch (Exception e10) {
e10.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
SamsungBindInstallAgentService.this.logger.mo2231d( SamsungBindInstallAgentService.this.getClass().getSimpleName() + " : onServiceDisconnected() called with: className = [" + componentName + "]");
SamsungBindInstallAgentService.this.installAgentAPI = null;
SamsungBindInstallAgentService.this.mBound = false;
SamsungBindInstallAgentService.this.isBinding = false;
}
};

Realme

Some versions of AppHub reference com.applovin.oem.p036am.device.realme.RealmeDownloader, which through its name indicates that it is a Realme library to perform downloads.

According to a May 28, 2023 change analysis, RealMe added AppHub to its phones beginning in its  F.07 update.

Cricket, Oppo, Orange, Sliide, Tinno

In AppHub code, I found references to Cricket, Oppo, Orange, Sliide, Tinno.  But I didn’t see full installation integrations for these carriers within the AppHub versions I received.  That said, it seems AppLovin provides a different AppHub APK for each partner.  When I bought a device from T-Mobile, I naturally received a phone with the T-Mobile AppHub APK.  The lack of other carriers’ AppHub APKs on a T-Mobile device should not be seen as a surprise.

An AppLovin press release describes a partnership with OPPO for “mobile app recommendations that “connect[] users with a wide variety of apps, from popular games to productivity tools, designed to cater to the unique preferences of each user.”  This could be a euphemism for installing games when users merely view ads for those apps.

Xiaomi and TCL

The Culper report discusses AppLovin AppHub partnerships with Xiaomi and TCL.  I did not find evidence of integration between AppHub and these companies in the code I reviewed.  Here too, there could be other APK versions that implement these integrations.