mConfirmIntentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
// User did some interaction and the installer screen is closed now
Intent broadcastIntent = new Intent(PackageInstallerCompat.ACTION_INSTALL_INTERACTION_END);
broadcastIntent.setPackage(getPackageName());
broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, mSessionId);
getApplicationContext().sendBroadcast(broadcastIntent);
if (!hasNext() && !mIsDealingWithApk) {
// No APKs left, this maybe a solo call
finish();
} // else let the original activity decide what to do
});
private final AccessibilityMultiplexer mMultiplexer = AccessibilityMultiplexer.getInstance();
private final StoragePermission mStoragePermission = StoragePermission.init(this);
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = ((ForegroundService.Binder) service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
};
@Override
public boolean getTransparentBackground() {
return true;
}
@Override
protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
final Intent intent = getIntent();
if (intent == null) {
triggerCancel();
return;
}
Log.d(TAG, "On create, intent: %s", intent);
if (ACTION_PACKAGE_INSTALLED.equals(intent.getAction())) {
onNewIntent(intent);
return;
}
mModel = new ViewModelProvider(this).get(PackageInstallerViewModel.class);
if (!bindService(
new Intent(this, PackageInstallerService.class), mServiceConnection, BIND_AUTO_CREATE)) {
throw new RuntimeException("Unable to bind PackageInstallerService");
}
synchronized (mApkQueue) {
mApkQueue.addAll(ApkQueueItem.fromIntent(intent, Utils.getRealReferrer(this)));
}
ApkSource apkSource = IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_APK_FILE_LINK, ApkSource.class);
if (apkSource != null) {
synchronized (mApkQueue) {
mApkQueue.add(ApkQueueItem.fromApkSource(apkSource));
}
}
mModel.packageInfoLiveData().observe(this, newPackageInfo -> {
if (newPackageInfo == null) {
mDialogHelper.showParseFailedDialog(v -> triggerCancel());
return;
}
// TODO: Resolve dependencies
mDialogHelper.onParseSuccess(mModel.getAppLabel(), getVersionInfoWithTrackers(newPackageInfo),
mModel.getAppIcon(), v -> displayInstallerOptions((dialog1, which, options) -> {
if (options != null) {
mInstallerOptions.copy(options);
}
}));
displayChangesOrInstallationPrompt();
});
mModel.packageUninstalledLiveData().observe(this, success -> {
if (success) {
install();
} else {
showInstallationFinishedDialog(mModel.getPackageName(), getString(R.string.failed_to_uninstall_app),
null, false);
}
});
// Init fragment
mInstallerDialogFragment = new InstallerDialogFragment();
mInstallerDialogFragment.setCancelable(false);
mInstallerDialogFragment.setFragmentStartedCallback(this::init);
mInstallerDialogFragment.showNow(getSupportFragmentManager(), InstallerDialogFragment.TAG);
}
@Override
protected void onDestroy() {
if (mService != null) {
unbindService(mServiceConnection);
}
unsetInstallFinishedListener();
// Delete remaining cached file
if (mCurrentItem != null && (mCurrentItem.getApkSource() instanceof CachedApkSource)) {
((CachedApkSource) mCurrentItem.getApkSource()).cleanup();
}
super.onDestroy();
}
private void init(@NonNull InstallerDialogFragment fragment, @NonNull AlertDialog dialog) {
// Make sure that it's only initiated once
if (initiated) {
return;
}
initiated = true;
mDialogHelper = new InstallerDialogHelper(fragment, dialog);
mDialogHelper.initProgress(v -> triggerCancel());
goToNext();
}
@UiThread
private void displayChangesOrInstallationPrompt() {
// This dialog either calls triggerInstall() or triggerCancel()
boolean displayChanges;
PackageInfo installedPackageInfo = mModel.getInstalledPackageInfo();
int actionRes;
if (installedPackageInfo == null) {
// App not installed or data not cleared
displayChanges = false;
actionRes = R.string.install;
} else {
// App is installed or the app is uninstalled without clearing data, or the app is uninstalled,
// but it's a system app
long installedVersionCode = PackageInfoCompat.getLongVersionCode(installedPackageInfo);
long thisVersionCode = PackageInfoCompat.getLongVersionCode(mModel.getNewPackageInfo());
displayChanges = Prefs.Installer.displayChanges();
if (installedVersionCode < thisVersionCode) {
// Needs update
actionRes = R.string.update;
} else if (installedVersionCode == thisVersionCode) {
// Issue reinstall
actionRes = R.string.reinstall;
} else {
// Downgrade
actionRes = R.string.downgrade;
}
}
if (displayChanges) {
WhatsNewFragment dialogFragment = WhatsNewFragment.getInstance(mModel.getNewPackageInfo(),
mModel.getInstalledPackageInfo());
mDialogHelper.showWhatsNewDialog(actionRes, dialogFragment, new InstallerDialogHelper.OnClickButtonsListener() {
@Override
public void triggerInstall() {
displayInstallationPrompt(actionRes, true);
}
@Override
public void triggerCancel() {
PackageInstallerActivity.this.triggerCancel();
}
}, mAppInfoClickListener);
return;
}
displayInstallationPrompt(actionRes, false);
}
private void displayInstallationPrompt(int actionRes, boolean splitOnly) {
if (mModel.getApkFile().isSplit()) {
SplitApkChooser fragment = SplitApkChooser.getNewInstance(getVersionInfoWithTrackers(
mModel.getNewPackageInfo()), getString(actionRes));
mDialogHelper.showApkChooserDialog(actionRes, fragment, this, mAppInfoClickListener);
return;
}
if (!splitOnly) {
// In unprivileged mode, a dialog is generated by the system. But we need to display it nonetheless in order
// to provide additional features.
mDialogHelper.showInstallConfirmationDialog(actionRes, this, mAppInfoClickListener);
} else triggerInstall();
}
private void displayInstallerOptions(InstallerOptionsFragment.OnClickListener clickListener) {
PackageInfo packageInfo = mModel.getNewPackageInfo();
InstallerOptionsFragment dialog = InstallerOptionsFragment.getInstance(packageInfo.packageName,
ApplicationInfoCompat.isTestOnly(packageInfo.applicationInfo), mInstallerOptions, clickListener);
dialog.show(getSupportFragmentManager(), InstallerOptionsFragment.TAG);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
outState.clear();
super.onSaveInstanceState(outState);
}
@UiThread
private void install() {
if (mModel.getApkFile().hasObb() && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) {
// Need to request permissions if not given
mStoragePermission.request(granted -> {
if (granted) launchInstallerService();
});
} else launchInstallerService();
}
@UiThread
private void launchInstallerService() {
assert mCurrentItem != null;
int userId = mInstallerOptions.getUserId();
mCurrentItem.setInstallerOptions(mInstallerOptions);
mCurrentItem.setSelectedSplits(mModel.getSelectedSplitsForInstallation());
mLastUserId = userId == UserHandleHidden.USER_ALL ? UserHandleHidden.myUserId() : userId;
boolean canDisplayNotification = Utils.canDisplayNotification(this);
boolean alwaysOnBackground = canDisplayNotification && Prefs.Installer.installInBackground();
Intent intent = new Intent(this, PackageInstallerService.class);
IntentCompat.putWrappedParcelableExtra(intent, PackageInstallerService.EXTRA_QUEUE_ITEM, mCurrentItem);
if (!SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) {
// For unprivileged mode, use accessibility service if enabled
mMultiplexer.enableInstall(true);
}
ContextCompat.startForegroundService(this, intent);
if (!alwaysOnBackground && mService != null) {
setInstallFinishedListener();
mDialogHelper.showInstallProgressDialog(canDisplayNotification ? v -> {
unsetInstallFinishedListener();
goToNext();
} : null);
} else {
unsetInstallFinishedListener();
// For some reason, the service is empty
// Install next app instead
goToNext();
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(TAG, "New intent called: %s", intent);
setIntent(intent);
// Check for action first
if (ACTION_PACKAGE_INSTALLED.equals(intent.getAction())) {
mSessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
mPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME);
Intent confirmIntent = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent.class);
try {
if (mPackageName == null || confirmIntent == null) throw new Exception("Empty confirmation intent.");
Log.d(TAG, "Requesting user confirmation for package %s", mPackageName);
mConfirmIntentLauncher.launch(confirmIntent);
} catch (Exception e) {
e.printStackTrace();
PackageInstallerCompat.sendCompletedBroadcast(this, mPackageName, PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE_ROM, mSessionId);
if (!hasNext() && !mIsDealingWithApk) {
// No APKs left, this maybe a solo call
finish();
} // else let the original activity decide what to do
}
return;
}
// New APK files added
synchronized (mApkQueue) {
mApkQueue.addAll(ApkQueueItem.fromIntent(intent, Utils.getRealReferrer(this)));
}
UIUtils.displayShortToast(R.string.added_to_queue);
}
@UiThread
@Override
public void triggerInstall() {
// Calls install(), reinstall() (which in terms called install()) and triggerCancel()
if (mModel.getInstalledPackageInfo() == null) {
// App not installed
install();
return;
}
InstallerDialogHelper.OnClickButtonsListener reinstallListener = new InstallerDialogHelper.OnClickButtonsListener() {
@Override
public void triggerInstall() {
// Uninstall and then install again
reinstall();
}
@Override
public void triggerCancel() {
PackageInstallerActivity.this.triggerCancel();
}
};
long installedVersionCode = PackageInfoCompat.getLongVersionCode(mModel.getInstalledPackageInfo());
long thisVersionCode = PackageInfoCompat.getLongVersionCode(mModel.getNewPackageInfo());
if (installedVersionCode > thisVersionCode && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES)) {
// Need to uninstall and install again
SpannableStringBuilder builder = new SpannableStringBuilder()
.append(getString(R.string.do_you_want_to_uninstall_and_install)).append(" ")
.append(UIUtils.getItalicString(getString(R.string.app_data_will_be_lost)))
.append("\n\n");
mDialogHelper.showDowngradeReinstallWarning(builder, reinstallListener, mAppInfoClickListener);
return;
}
if (!mModel.isSignatureDifferent()) {
// Signature is either matched or the app isn't installed
install();
return;
}
// Signature is different
ApplicationInfo info = mModel.getInstalledPackageInfo().applicationInfo; // Installed package info is never null here.
boolean isSystem = ApplicationInfoCompat.isSystemApp(info);
SpannableStringBuilder builder = new SpannableStringBuilder();
if (isSystem) {
// Cannot reinstall a system app with a different signature
builder.append(getString(R.string.app_signing_signature_mismatch_for_system_apps));
} else {
// Offer user to uninstall and then install the app again
builder.append(getString(R.string.do_you_want_to_uninstall_and_install)).append(" ")
.append(UIUtils.getItalicString(getString(R.string.app_data_will_be_lost)));
}
builder.append("\n\n");
int start = builder.length();
builder.append(getText(R.string.app_signing_install_without_data_loss));
builder.setSpan(new RelativeSizeSpan(0.8f), start, builder.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
mDialogHelper.showSignatureMismatchReinstallWarning(builder, reinstallListener, v -> install(), isSystem);
}
@Override
public void triggerCancel() {
// Run cleanup
if (mCurrentItem != null && mCurrentItem.getApkSource() instanceof CachedApkSource) {
((CachedApkSource) mCurrentItem.getApkSource()).cleanup();
}
goToNext();
}
private void reinstall() {
if (!SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES)) {
mMultiplexer.enableUninstall(true);
}
mModel.uninstallPackage();
}
/**
* Closes the current APK and start the next
*/
private void goToNext() {
mCurrentItem = null;
mMultiplexer.enableInstall(false);
mMultiplexer.enableUninstall(false);
if (hasNext()) {
mIsDealingWithApk = true;
mDialogHelper.initProgress(v -> goToNext());
synchronized (mApkQueue) {
mCurrentItem = Objects.requireNonNull(mApkQueue.poll());
mModel.getPackageInfo(mCurrentItem);
}
} else {
mIsDealingWithApk = false;
mDialogHelper.dismiss();
finish();
}
}
private boolean hasNext() {
synchronized (mApkQueue) {
return !mApkQueue.isEmpty();
}
}
@NonNull
private String getVersionInfoWithTrackers(@NonNull final PackageInfo newPackageInfo) {
Resources res = getApplication().getResources();
long newVersionCode = PackageInfoCompat.getLongVersionCode(newPackageInfo);
String newVersionName = newPackageInfo.versionName;
int trackers = mModel.getTrackerCount();
StringBuilder sb = new StringBuilder(res.getString(R.string.version_name_with_code, newVersionName, newVersionCode));
if (trackers > 0) {
sb.append(", ").append(res.getQuantityString(R.plurals.no_of_trackers, trackers, trackers));
}
return sb.toString();
}
public void showInstallationFinishedDialog(String packageName, int result, @Nullable String blockingPackage,
@Nullable String statusMessage) {
showInstallationFinishedDialog(packageName, getStringFromStatus(result, blockingPackage), statusMessage,
result == STATUS_SUCCESS);
}
public void showInstallationFinishedDialog(String packageName, CharSequence message,
@Nullable String statusMessage, boolean displayOpenAndAppInfo) {
SpannableStringBuilder ssb = new SpannableStringBuilder(message);
if (statusMessage != null) {
ssb.append("\n\n").append(UIUtils.getItalicString(statusMessage));
}
Intent intent = PackageManagerCompat.getLaunchIntentForPackage(packageName, UserHandleHidden.myUserId());
mDialogHelper.showInstallFinishedDialog(ssb, hasNext() ? R.string.next : R.string.close, v -> goToNext(),
displayOpenAndAppInfo && intent != null ? v -> {
try {
startActivity(intent);
} catch (Throwable th) {
UIUtils.displayLongToast(th.getMessage());
} finally {
goToNext();
}
} : null, displayOpenAndAppInfo ? v -> {
try {
Intent appDetailsIntent = AppDetailsActivity.getIntent(this, packageName, mLastUserId, true);
appDetailsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(appDetailsIntent);
} finally {
goToNext();
}
} : null);
}
@NonNull
private String getStringFromStatus(@PackageInstallerCompat.Status int status,
@Nullable String blockingPackage) {
switch (status) {
case STATUS_SUCCESS:
return getString(R.string.installer_app_installed);
case STATUS_FAILURE_ABORTED:
return getString(R.string.installer_error_aborted);
case STATUS_FAILURE_BLOCKED:
String blocker = getString(R.string.installer_error_blocked_device);
if (blockingPackage != null) {
blocker = PackageUtils.getPackageLabel(getPackageManager(), blockingPackage);
}
return getString(R.string.installer_error_blocked, blocker);
case STATUS_FAILURE_CONFLICT:
return getString(R.string.installer_error_conflict);
case STATUS_FAILURE_INCOMPATIBLE:
return getString(R.string.installer_error_incompatible);
case STATUS_FAILURE_INVALID:
return getString(R.string.installer_error_bad_apks);
case STATUS_FAILURE_STORAGE:
return getString(R.string.installer_error_storage);
case STATUS_FAILURE_SECURITY:
return getString(R.string.installer_error_security);
case STATUS_FAILURE_SESSION_CREATE:
return getString(R.string.installer_error_session_create);
case STATUS_FAILURE_SESSION_WRITE:
return getString(R.string.installer_error_session_write);
case STATUS_FAILURE_SESSION_COMMIT:
return getString(R.string.installer_error_session_commit);
case STATUS_FAILURE_SESSION_ABANDON:
return getString(R.string.installer_error_session_abandon);
case STATUS_FAILURE_INCOMPATIBLE_ROM:
return getString(R.string.installer_error_lidl_rom);
}
return getString(R.string.installer_error_generic);
}
public void setInstallFinishedListener() {
if (mService != null) {
mService.setOnInstallFinished((packageName, status, blockingPackage, statusMessage) -> {
if (isFinishing()) return;
showInstallationFinishedDialog(packageName, status, blockingPackage, statusMessage);
});
}
}
public void unsetInstallFinishedListener() {
if (mService != null) {
mService.setOnInstallFinished(null);
}
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerBroadcastReceiver.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.installer;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import androidx.annotation.NonNull;
import androidx.core.app.PendingIntentCompat;
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.intercept.IntentCompat;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.AppManager.utils.ContextUtils;
import io.github.muntashirakon.AppManager.utils.NotificationUtils;
import io.github.muntashirakon.AppManager.utils.Utils;
class PackageInstallerBroadcastReceiver extends BroadcastReceiver {
public static final String TAG = PackageInstallerBroadcastReceiver.class.getSimpleName();
public static final String ACTION_PI_RECEIVER = BuildConfig.APPLICATION_ID + ".action.PI_RECEIVER";
private String mPackageName;
private CharSequence mAppLabel;
private int mConfirmNotificationId = 0;
public void setPackageName(String packageName) {
mPackageName = packageName;
}
public void setAppLabel(CharSequence appLabel) {
mAppLabel = appLabel;
}
@Override
public void onReceive(Context nullableContext, @NonNull Intent intent) {
Context context = nullableContext != null ? nullableContext : ContextUtils.getContext();
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1);
int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
Log.d(TAG, "Session ID: %d", sessionId);
switch (status) {
case PackageInstaller.STATUS_PENDING_USER_ACTION:
Log.d(TAG, "Requesting user confirmation...");
// Send broadcast first
Intent broadcastIntent2 = new Intent(PackageInstallerCompat.ACTION_INSTALL_INTERACTION_BEGIN);
broadcastIntent2.setPackage(context.getPackageName());
broadcastIntent2.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
broadcastIntent2.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
context.sendBroadcast(broadcastIntent2);
// Open confirmIntent using the PackageInstallerActivity.
// If the confirmIntent isn't open via an activity, it will fail for large apk files
Intent confirmIntent = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent.class);
Intent intent2 = new Intent(context, PackageInstallerActivity.class);
intent2.setAction(PackageInstallerActivity.ACTION_PACKAGE_INSTALLED);
intent2.putExtra(Intent.EXTRA_INTENT, confirmIntent);
intent2.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
intent2.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
boolean appInForeground = Utils.isAppInForeground();
if (appInForeground) {
// Open activity directly and issue a silent notification
context.startActivity(intent2);
}
// Delete intent: aborts the operation
Intent broadcastCancel = new Intent(PackageInstallerCompat.ACTION_INSTALL_COMPLETED);
broadcastCancel.setPackage(context.getPackageName());
broadcastCancel.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
broadcastCancel.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstallerCompat.STATUS_FAILURE_ABORTED);
broadcastCancel.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
// Ask user for permission
mConfirmNotificationId = NotificationUtils.displayInstallConfirmNotification(context, builder -> builder
.setAutoCancel(false)
.setSilent(appInForeground)
.setDefaults(Notification.DEFAULT_ALL)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_default_notification)
.setTicker(mAppLabel)
.setContentTitle(mAppLabel)
.setSubText(context.getString(R.string.package_installer))
// A neat way to find the title is to check for sessionId
.setContentText(context.getString(sessionId == -1 ? R.string.confirm_uninstallation : R.string.confirm_installation))
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent2,
PendingIntent.FLAG_UPDATE_CURRENT, false))
.setDeleteIntent(PendingIntentCompat.getBroadcast(context, 0, broadcastCancel,
PendingIntent.FLAG_UPDATE_CURRENT, false))
.build());
break;
case PackageInstaller.STATUS_SUCCESS:
Log.d(TAG, "Install success!");
NotificationUtils.cancelInstallConfirmNotification(context, mConfirmNotificationId);
PackageInstallerCompat.sendCompletedBroadcast(context, mPackageName, PackageInstallerCompat.STATUS_SUCCESS, sessionId);
break;
default:
NotificationUtils.cancelInstallConfirmNotification(context, mConfirmNotificationId);
Intent broadcastError = new Intent(PackageInstallerCompat.ACTION_INSTALL_COMPLETED);
broadcastError.setPackage(context.getPackageName());
String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
broadcastError.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, statusMessage);
broadcastError.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, mPackageName);
broadcastError.putExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME, intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME));
broadcastError.putExtra(PackageInstaller.EXTRA_STATUS, status);
broadcastError.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
context.sendBroadcast(broadcastError);
Log.d(TAG, "Install failed! %s", statusMessage);
break;
}
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.installer;
import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageInstaller;
import android.content.pm.IPackageInstallerSession;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageInstallerHidden;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandleHidden;
import android.provider.Settings;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.app.PendingIntentCompat;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import aosp.libcore.util.EmptyArray;
import dev.rikka.tools.refine.Refine;
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.apk.ApkFile;
import io.github.muntashirakon.AppManager.apk.ApkUtils;
import io.github.muntashirakon.AppManager.compat.ManifestCompat;
import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
import io.github.muntashirakon.AppManager.ipc.ProxyBinder;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.AppManager.progress.ProgressHandler;
import io.github.muntashirakon.AppManager.self.SelfPermissions;
import io.github.muntashirakon.AppManager.settings.Ops;
import io.github.muntashirakon.AppManager.types.UserPackagePair;
import io.github.muntashirakon.AppManager.users.Users;
import io.github.muntashirakon.AppManager.utils.BroadcastUtils;
import io.github.muntashirakon.AppManager.utils.ContextUtils;
import io.github.muntashirakon.AppManager.utils.ExUtils;
import io.github.muntashirakon.AppManager.utils.FileUtils;
import io.github.muntashirakon.AppManager.utils.HuaweiUtils;
import io.github.muntashirakon.AppManager.utils.MiuiUtils;
import io.github.muntashirakon.AppManager.utils.PackageUtils;
import io.github.muntashirakon.AppManager.utils.ThreadUtils;
import io.github.muntashirakon.AppManager.utils.UIUtils;
import io.github.muntashirakon.io.Path;
@SuppressLint("ShiftFlags")
public final class PackageInstallerCompat {
public static final String TAG = PackageInstallerCompat.class.getSimpleName();
public static final String ACTION_INSTALL_STARTED = BuildConfig.APPLICATION_ID + ".action.INSTALL_STARTED";
public static final String ACTION_INSTALL_COMPLETED = BuildConfig.APPLICATION_ID + ".action.INSTALL_COMPLETED";
// For rootless installer to prevent PackageInstallerService from hanging
public static final String ACTION_INSTALL_INTERACTION_BEGIN = BuildConfig.APPLICATION_ID + ".action.INSTALL_INTERACTION_BEGIN";
public static final String ACTION_INSTALL_INTERACTION_END = BuildConfig.APPLICATION_ID + ".action.INSTALL_INTERACTION_END";
@IntDef({
STATUS_SUCCESS,
STATUS_FAILURE_ABORTED,
STATUS_FAILURE_BLOCKED,
STATUS_FAILURE_CONFLICT,
STATUS_FAILURE_INCOMPATIBLE,
STATUS_FAILURE_INVALID,
STATUS_FAILURE_STORAGE,
// Custom
STATUS_FAILURE_SECURITY,
STATUS_FAILURE_SESSION_CREATE,
STATUS_FAILURE_SESSION_WRITE,
STATUS_FAILURE_SESSION_COMMIT,
STATUS_FAILURE_SESSION_ABANDON,
STATUS_FAILURE_INCOMPATIBLE_ROM,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {
}
/**
* See {@link PackageInstaller#STATUS_SUCCESS}
*/
public static final int STATUS_SUCCESS = PackageInstaller.STATUS_SUCCESS;
/**
* See {@link PackageInstaller#STATUS_FAILURE_ABORTED}
*/
public static final int STATUS_FAILURE_ABORTED = PackageInstaller.STATUS_FAILURE_ABORTED;
/**
* See {@link PackageInstaller#STATUS_FAILURE_BLOCKED}
*/
public static final int STATUS_FAILURE_BLOCKED = PackageInstaller.STATUS_FAILURE_BLOCKED;
/**
* See {@link PackageInstaller#STATUS_FAILURE_CONFLICT}
*/
public static final int STATUS_FAILURE_CONFLICT = PackageInstaller.STATUS_FAILURE_CONFLICT;
/**
* See {@link PackageInstaller#STATUS_FAILURE_INCOMPATIBLE}
*/
public static final int STATUS_FAILURE_INCOMPATIBLE = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
/**
* See {@link PackageInstaller#STATUS_FAILURE_INVALID}
*/
public static final int STATUS_FAILURE_INVALID = PackageInstaller.STATUS_FAILURE_INVALID;
/**
* See {@link PackageInstaller#STATUS_FAILURE_STORAGE}
*/
public static final int STATUS_FAILURE_STORAGE = PackageInstaller.STATUS_FAILURE_STORAGE;
// Custom status
/**
* The operation failed because the apk file(s) are not accessible.
*/
public static final int STATUS_FAILURE_SECURITY = -2;
/**
* The operation failed because it failed to create an installer session.
*/
public static final int STATUS_FAILURE_SESSION_CREATE = -3;
/**
* The operation failed because it failed to write apk files to session.
*/
public static final int STATUS_FAILURE_SESSION_WRITE = -4;
/**
* The operation failed because it could not commit the installer session.
*/
public static final int STATUS_FAILURE_SESSION_COMMIT = -5;
/**
* The operation failed because it could not abandon the installer session. This is a redundant
* failure.
*/
public static final int STATUS_FAILURE_SESSION_ABANDON = -6;
/**
* The operation failed because the current ROM is incompatible with PackageInstaller
*/
public static final int STATUS_FAILURE_INCOMPATIBLE_ROM = -7;
@SuppressLint({"NewApi", "UniqueConstants", "InlinedApi"})
@IntDef(flag = true, value = {
INSTALL_REPLACE_EXISTING,
INSTALL_ALLOW_TEST,
INSTALL_EXTERNAL,
INSTALL_INTERNAL,
INSTALL_FROM_ADB,
INSTALL_ALL_USERS,
INSTALL_REQUEST_DOWNGRADE,
INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS,
INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS,
INSTALL_FORCE_VOLUME_UUID,
INSTALL_FORCE_PERMISSION_PROMPT,
INSTALL_INSTANT_APP,
INSTALL_DONT_KILL_APP,
INSTALL_FULL_APP,
INSTALL_ALLOCATE_AGGRESSIVE,
INSTALL_VIRTUAL_PRELOAD,
INSTALL_APEX,
INSTALL_ENABLE_ROLLBACK,
INSTALL_DISABLE_VERIFICATION,
INSTALL_ALLOW_DOWNGRADE,
INSTALL_ALLOW_DOWNGRADE_API29,
INSTALL_STAGED,
INSTALL_DRY_RUN,
INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK,
INSTALL_REQUEST_UPDATE_OWNERSHIP,
INSTALL_FROM_MANAGED_USER_OR_PROFILE,
INSTALL_IGNORE_DEXOPT_PROFILE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface InstallFlags {
}
/**
* Flag parameter for {@code #installPackage} to indicate that you want to replace an already
* installed package, if one exists.
*/
public static final int INSTALL_REPLACE_EXISTING = 0x00000002;
/**
* Flag parameter for {@code #installPackage} to indicate that you want to
* allow test packages (those that have set android:testOnly in their
* manifest) to be installed.
*/
public static final int INSTALL_ALLOW_TEST = 0x00000004;
/**
* Flag parameter for {@code #installPackage} to indicate that this
* package has to be installed on the sdcard.
*
* @deprecated Removed in API 29 (Android 10)
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public static final int INSTALL_EXTERNAL = 0x00000008;
/**
* Flag parameter for {@code #installPackage} to indicate that this package
* has to be installed on the sdcard.
*/
public static final int INSTALL_INTERNAL = 0x00000010;
/**
* Flag parameter for {@code #installPackage} to indicate that this install
* was initiated via ADB.
*/
public static final int INSTALL_FROM_ADB = 0x00000020;
/**
* Flag parameter for {@code #installPackage} to indicate that this install
* should immediately be visible to all users.
*/
public static final int INSTALL_ALL_USERS = 0x00000040;
/**
* Flag parameter for {@code #installPackage} to indicate that an upgrade to a lower version
* of a package than currently installed has been requested.
*
* Note that this flag doesn't guarantee that downgrade will be performed. That decision
* depends
* on whenever:
*
* - An app is debuggable.
*
- Or a build is debuggable.
*
- Or {@link #INSTALL_ALLOW_DOWNGRADE} is set.
*
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_REQUEST_DOWNGRADE = 0x00000080;
/**
* Flag parameter for {@code #installPackage} to indicate that all runtime
* permissions should be granted to the package. If {@link #INSTALL_ALL_USERS}
* is set the runtime permissions will be granted to all users, otherwise
* only to the owner.
*
* Previously called {@code #INSTALL_GRANT_RUNTIME_PERMISSIONS}
*/
@RequiresApi(Build.VERSION_CODES.M)
public static final int INSTALL_GRANT_ALL_REQUESTED_PERMISSIONS = 0x00000100;
/**
* Flag parameter for {@code #installPackage} to indicate that all restricted
* permissions should be whitelisted. If {@link #INSTALL_ALL_USERS}
* is set the restricted permissions will be whitelisted for all users, otherwise
* only to the owner.
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS = 0x00400000;
@RequiresApi(Build.VERSION_CODES.M)
public static final int INSTALL_FORCE_VOLUME_UUID = 0x00000200;
/**
* Flag parameter for {@code #installPackage} to indicate that we always want to force
* the prompt for permission approval. This overrides any special behaviour for internal
* components.
*/
@RequiresApi(Build.VERSION_CODES.N)
public static final int INSTALL_FORCE_PERMISSION_PROMPT = 0x00000400;
/**
* Flag parameter for {@code #installPackage} to indicate that this package is
* to be installed as a lightweight "ephemeral" app.
*
* Previously known as {@code #INSTALL_EPHEMERAL}
*/
@RequiresApi(Build.VERSION_CODES.N)
public static final int INSTALL_INSTANT_APP = 0x00000800;
/**
* Flag parameter for {@code #installPackage} to indicate that this package contains
* a feature split to an existing application and the existing application should not
* be killed during the installation process.
*/
@RequiresApi(Build.VERSION_CODES.N)
public static final int INSTALL_DONT_KILL_APP = 0x00001000;
/**
* Flag parameter for {@code #installPackage} to indicate that this package is an
* upgrade to a package that refers to the SDK via release letter.
*
* @deprecated Removed in API 29 (Android 10)
*/
@Deprecated
@RequiresApi(Build.VERSION_CODES.N)
public static final int INSTALL_FORCE_SDK = 0x00002000;
/**
* Flag parameter for {@code #installPackage} to indicate that this package is
* to be installed as a heavy weight app. This is fundamentally the opposite of
* {@link #INSTALL_INSTANT_APP}.
*/
@RequiresApi(Build.VERSION_CODES.O)
public static final int INSTALL_FULL_APP = 0x00004000;
/**
* Flag parameter for {@code #installPackage} to indicate that this package
* is critical to system health or security, meaning the system should use
* {@code StorageManager#FLAG_ALLOCATE_AGGRESSIVE} internally.
*/
@RequiresApi(Build.VERSION_CODES.O)
public static final int INSTALL_ALLOCATE_AGGRESSIVE = 0x00008000;
/**
* Flag parameter for {@code #installPackage} to indicate that this package
* is a virtual preload.
*/
@RequiresApi(Build.VERSION_CODES.O_MR1)
public static final int INSTALL_VIRTUAL_PRELOAD = 0x00010000;
/**
* Flag parameter for {@code #installPackage} to indicate that this package
* is an APEX package
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_APEX = 0x00020000;
/**
* Flag parameter for {@code #installPackage} to indicate that rollback
* should be enabled for this install.
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_ENABLE_ROLLBACK = 0x00040000;
/**
* Flag parameter for {@code #installPackage} to indicate that package verification should be
* disabled for this package.
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_DISABLE_VERIFICATION = 0x00080000;
/**
* Flag parameter for {@code #installPackage} to indicate that
* {@link #INSTALL_REQUEST_DOWNGRADE} should be allowed.
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_ALLOW_DOWNGRADE_API29 = 0x00100000;
/**
* Flag parameter for {@code #installPackage} to indicate that this package
* is being installed as part of a staged install.
*/
@RequiresApi(Build.VERSION_CODES.Q)
public static final int INSTALL_STAGED = 0x00200000;
/**
* Flag parameter for {@code #installPackage} to indicate that package should only be verified
* but not installed.
*
* @deprecated Removed in API 30 (Android 11)
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@RequiresApi(Build.VERSION_CODES.Q)
@Deprecated
public static final int INSTALL_DRY_RUN = 0x00800000;
/**
* Flag parameter for {@code #installPackage} to indicate that it is okay
* to install an update to an app where the newly installed app has a lower
* version code than the currently installed app.
*
* @deprecated Replaced by {@link #INSTALL_ALLOW_DOWNGRADE_API29} in Android 10
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public static final int INSTALL_ALLOW_DOWNGRADE = 0x00000080;
/**
* Flag parameter for {@code #installPackage} to bypass the low targer sdk version block
* for this install.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x01000000;
/**
* Flag parameter for {@link SessionParams} to indicate that the
* update ownership enforcement is requested.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final int INSTALL_REQUEST_UPDATE_OWNERSHIP = 1 << 25;
/**
* Flag parameter for {@link SessionParams} to indicate that this
* session is from a managed user or profile.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26;
/**
* If set, all dexopt profiles are ignored by dexopt during the installation, including the
* profile in the DM file and the profile embedded in the APK file. If an invalid profile is
* provided during installation, no warning will be reported by {@code adb install}.
*
* This option does not affect later dexopt operations (e.g., background dexopt and manual `pm
* compile` invocations).
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final int INSTALL_IGNORE_DEXOPT_PROFILE = 1 << 28;
@SuppressLint({"NewApi", "InlinedApi"})
@IntDef(flag = true, value = {
DELETE_KEEP_DATA,
DELETE_ALL_USERS,
DELETE_SYSTEM_APP,
DELETE_DONT_KILL_APP,
DELETE_CHATTY,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DeleteFlags {
}
/**
* Flag parameter for {@code #deletePackage} to indicate that you don't want to delete the
* package's data directory.
*/
public static final int DELETE_KEEP_DATA = 0x00000001;
/**
* Flag parameter for {@code #deletePackage} to indicate that you want the
* package deleted for all users.
*/
public static final int DELETE_ALL_USERS = 0x00000002;
/**
* Flag parameter for {@code #deletePackage} to indicate that, if you are calling
* uninstall on a system that has been updated, then don't do the normal process
* of uninstalling the update and rolling back to the older system version (which
* needs to happen for all users); instead, just mark the app as uninstalled for
* the current user.
*/
public static final int DELETE_SYSTEM_APP = 0x00000004;
/**
* Flag parameter for {@code #deletePackage} to indicate that, if you are calling
* uninstall on a package that is replaced to provide new feature splits, the
* existing application should not be killed during the removal process.
*/
@RequiresApi(Build.VERSION_CODES.N)
public static final int DELETE_DONT_KILL_APP = 0x00000008;
/**
* Flag parameter for {@code #deletePackage} to indicate that package deletion
* should be chatty.
*/
@RequiresApi(Build.VERSION_CODES.P)
public static final int DELETE_CHATTY = 0x80000000;
public static final String SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS = "verifier_verify_adb_installs";
public interface OnInstallListener {
@WorkerThread
void onStartInstall(int sessionId, String packageName);
// MIUI-begin: MIUI 12.5+ workaround
/**
* MIUI 12.5+ may require more than one tries in order to have successful installations.
* This is only needed during APK installations, not APK uninstallations or install-existing
* attempts.
*
* @param apkFile Underlying APK file if available.
*/
@WorkerThread
default void onAnotherAttemptInMiui(@Nullable ApkFile apkFile) {
}
// MIUI-end
// HyperOS-begin: HyperOS 2.0+ workaround
/**
* In HyperOS 2.0+, the installer for the system apps must be another system app. The
* overridden method must set the package installer to a valid system app. This is only
* needed during APK installations, not APK uninstallations or install-existing attempts.
*
* @param apkFile Underlying APK file if available.
*/
@WorkerThread
default void onSecondAttemptInHyperOsWithoutInstaller(@Nullable ApkFile apkFile) {
}
// HyperOS-end
@WorkerThread
void onFinishedInstall(int sessionId, String packageName, int result, @Nullable String blockingPackage,
@Nullable String statusMessage);
}
@NonNull
public static PackageInstallerCompat getNewInstance() {
return new PackageInstallerCompat();
}
private CountDownLatch mInstallWatcher;
private CountDownLatch mInteractionWatcher;
private boolean mCloseApkFile = true;
private boolean mInstallCompleted = false;
@Nullable
private ApkFile mApkFile;
private String mPackageName;
@Nullable
private CharSequence mAppLabel;
private int mSessionId = -1;
@Status
private int mFinalStatus = STATUS_FAILURE_INVALID;
@Nullable
private String mStatusMessage;
private PackageInstallerBroadcastReceiver mPkgInstallerReceiver;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, @NonNull Intent intent) {
if (intent.getAction() == null) return;
int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
Log.d(TAG, "Action: %s", intent.getAction());
Log.d(TAG, "Session ID: %d", sessionId);
switch (intent.getAction()) {
case ACTION_INSTALL_STARTED:
// Session successfully created
if (mOnInstallListener != null) {
mOnInstallListener.onStartInstall(sessionId, mPackageName);
}
break;
case ACTION_INSTALL_INTERACTION_BEGIN:
// An installation prompt is being shown to the user
// Run indefinitely until user finally decides to do something about it
break;
case ACTION_INSTALL_INTERACTION_END:
// The installation prompt is hidden by the user, either by clicking cancel or install,
// or just clicking on some place else (latter is our main focus)
if (mSessionId == sessionId) {
// The user interaction is done, it doesn't take more than 1 minute now
mInteractionWatcher.countDown();
}
break;
case ACTION_INSTALL_COMPLETED:
// Either it failed to create a session or the installation was completed,
// regardless of the status: success or failure
mFinalStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, STATUS_FAILURE_INVALID);
String blockingPackage = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME);
mStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
// Run install completed
mInstallCompleted = true;
ThreadUtils.postOnBackgroundThread(() ->
installCompleted(sessionId, mFinalStatus, blockingPackage, mStatusMessage));
break;
}
}
};
@Nullable
private OnInstallListener mOnInstallListener;
private IPackageInstaller mPackageInstaller;
private PackageInstaller.Session mSession;
// MIUI-added: Multiple attempts may be required
int mAttempts = 1;
private final Context mContext;
private final boolean mHasInstallPackagePermission;
private int mLastVerifyAdbInstallsResult;
private PackageInstallerCompat() {
mContext = ContextUtils.getContext();
mHasInstallPackagePermission = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.INSTALL_PACKAGES);
mLastVerifyAdbInstallsResult = -1;
}
public void setOnInstallListener(@Nullable OnInstallListener onInstallListener) {
mOnInstallListener = onInstallListener;
}
public void setAppLabel(@Nullable CharSequence appLabel) {
mAppLabel = appLabel;
}
@NonNull
private static int[] getAllRequestedUsers(int userId) {
switch (userId) {
case UserHandleHidden.USER_ALL:
return Users.getAllUserIds();
case UserHandleHidden.USER_NULL:
return EmptyArray.INT;
default:
return new int[]{userId};
}
}
public boolean install(@NonNull ApkFile apkFile, @NonNull List selectedSplitIds,
@NonNull InstallerOptions options, @Nullable ProgressHandler progressHandler) {
ThreadUtils.ensureWorkerThread();
try {
mApkFile = apkFile;
mPackageName = Objects.requireNonNull(apkFile.getPackageName());
initBroadcastReceiver();
int userId = options.getUserId();
int installFlags = getInstallFlags(userId);
int[] allRequestedUsers = getAllRequestedUsers(userId);
if (allRequestedUsers.length == 0) {
Log.d(TAG, "Install: no users.");
callFinish(STATUS_FAILURE_INVALID);
return false;
}
Log.d(TAG, "Installing for users: %s", Arrays.toString(allRequestedUsers));
for (int u : allRequestedUsers) {
if (!SelfPermissions.checkCrossUserPermission(u, true)) {
installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission.");
Log.d(TAG, "Install: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions.");
return false;
}
}
ThreadUtils.postOnBackgroundThread(() -> {
// TODO: 6/6/23 Wait for this task to finish before returning
// FIXME: 16/6/23 Needed only for one user?
for (int u : allRequestedUsers) {
copyObb(apkFile, u);
}
});
userId = allRequestedUsers[0];
String originatingPackage = options.isSetOriginatingPackage() ? options.getOriginatingPackage() : null;
Uri originatingUri = options.isSetOriginatingPackage() ? options.getOriginatingUri() : null;
Log.d(TAG, "Install: opening session...");
if (!openSession(userId, installFlags, options.getInstallerName(),
options.getInstallLocation(), originatingPackage, originatingUri,
options.getInstallScenario(), options.getPackageSource(),
options.requestUpdateOwnership(), options.isDisableApkVerification())) {
return false;
}
List selectedEntries = new ArrayList<>();
long totalSize = 0;
for (ApkFile.Entry entry : apkFile.getEntries()) {
if (selectedSplitIds.contains(entry.id)) {
selectedEntries.add(entry);
try {
totalSize += entry.getFile(options.isSignApkFiles()).length();
} catch (IOException e) {
callFinish(STATUS_FAILURE_INVALID);
Log.e(TAG, "Install: Cannot retrieve the selected APK files.", e);
return abandon();
}
}
}
Log.d(TAG, "Install: selected entries: %s", selectedSplitIds);
// Write apk files
for (ApkFile.Entry entry : selectedEntries) {
long entrySize = entry.getFileSize(options.isSignApkFiles());
try (InputStream apkInputStream = entry.getInputStream(options.isSignApkFiles());
OutputStream apkOutputStream = mSession.openWrite(entry.getFileName(), 0, entrySize)) {
FileUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler);
mSession.fsync(apkOutputStream);
Log.d(TAG, "Install: copied entry %s", entry.name);
} catch (IOException e) {
callFinish(STATUS_FAILURE_SESSION_WRITE);
Log.e(TAG, "Install: Cannot copy files to session.", e);
return abandon();
} catch (SecurityException e) {
callFinish(STATUS_FAILURE_SECURITY);
Log.e(TAG, "Install: Cannot access apk files.", e);
return abandon();
}
}
Log.d(TAG, "Install: Running installation...");
// Commit
return commit(userId);
} finally {
unregisterReceiver();
restoreVerifySettings();
}
}
public boolean install(@NonNull Path[] apkFiles, @NonNull String packageName, @NonNull InstallerOptions options) {
return install(apkFiles, packageName, options, null);
}
public boolean install(@NonNull Path[] apkFiles, @NonNull String packageName, @NonNull InstallerOptions options,
@Nullable ProgressHandler progressHandler) {
ThreadUtils.ensureWorkerThread();
try {
mApkFile = null;
mPackageName = Objects.requireNonNull(packageName);
initBroadcastReceiver();
int userId = options.getUserId();
int installFlags = getInstallFlags(userId);
int[] allRequestedUsers = getAllRequestedUsers(userId);
if (allRequestedUsers.length == 0) {
Log.d(TAG, "Install: no users.");
callFinish(STATUS_FAILURE_INVALID);
return false;
}
Log.d(TAG, "Installing for users: %s", Arrays.toString(allRequestedUsers));
for (int u : allRequestedUsers) {
if (!SelfPermissions.checkCrossUserPermission(u, true)) {
installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission.");
Log.d(TAG, "Install: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions.");
return false;
}
}
userId = allRequestedUsers[0];
String originatingPackage = options.isSetOriginatingPackage() ? options.getOriginatingPackage() : null;
Uri originatingUri = options.isSetOriginatingPackage() ? options.getOriginatingUri() : null;
if (!openSession(userId, installFlags, options.getInstallerName(),
options.getInstallLocation(), originatingPackage, originatingUri,
options.getInstallScenario(), options.getPackageSource(),
options.requestUpdateOwnership(), options.isDisableApkVerification())) {
return false;
}
long totalSize = 0;
for (Path apkFile : apkFiles) {
totalSize += apkFile.length();
}
// Write apk files
for (Path apkFile : apkFiles) {
try (InputStream apkInputStream = apkFile.openInputStream();
OutputStream apkOutputStream = mSession.openWrite(apkFile.getName(), 0, apkFile.length())) {
FileUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler);
mSession.fsync(apkOutputStream);
} catch (IOException e) {
callFinish(STATUS_FAILURE_SESSION_WRITE);
Log.e(TAG, "Install: Cannot copy files to session.", e);
return abandon();
} catch (SecurityException e) {
callFinish(STATUS_FAILURE_SECURITY);
Log.e(TAG, "Install: Cannot access apk files.", e);
return abandon();
}
}
// Commit
return commit(userId);
} finally {
unregisterReceiver();
restoreVerifySettings();
}
}
private boolean commit(int userId) {
IntentSender sender;
LocalIntentReceiver intentReceiver;
if (mHasInstallPackagePermission) {
Log.d(TAG, "Commit: Commit via LocalIntentReceiver...");
try {
intentReceiver = new LocalIntentReceiver();
sender = intentReceiver.getIntentSender();
} catch (Exception e) {
callFinish(STATUS_FAILURE_SESSION_COMMIT);
Log.e(TAG, "Commit: Could not commit session.", e);
return false;
}
} else {
Log.d(TAG, "Commit: Calling activity to request permission...");
intentReceiver = null;
Intent callbackIntent = new Intent(PackageInstallerBroadcastReceiver.ACTION_PI_RECEIVER);
callbackIntent.setPackage(BuildConfig.APPLICATION_ID);
PendingIntent pendingIntent = PendingIntentCompat.getBroadcast(mContext, 0, callbackIntent, 0, true);
sender = pendingIntent.getIntentSender();
}
Log.d(TAG, "Commit: Committing...");
try {
mSession.commit(sender);
} catch (Throwable e) { // primarily RemoteException
callFinish(STATUS_FAILURE_SESSION_COMMIT);
Log.e(TAG, "Commit: Could not commit session.", e);
return false;
}
if (intentReceiver == null) {
Log.d(TAG, "Commit: Waiting for user interaction...");
// Wait for user interaction (if needed)
try {
// Wait for user interaction
mInteractionWatcher.await();
// Wait for the installation to complete
mInstallWatcher.await(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Log.e(TAG, "Installation interrupted.", e);
}
} else {
Intent resultIntent = intentReceiver.getResult();
mFinalStatus = resultIntent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
mStatusMessage = resultIntent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
}
Log.d(TAG, "Commit: Finishing...");
// We might want to use {@code callFinish(finalStatus);} here, but it doesn't always work
// since the object is garbage collected almost immediately.
if (!mInstallCompleted) {
installCompleted(mSessionId, mFinalStatus, null, mStatusMessage);
}
if (mFinalStatus == PackageInstaller.STATUS_SUCCESS && userId != UserHandleHidden.myUserId()) {
BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{mPackageName});
}
return mFinalStatus == PackageInstaller.STATUS_SUCCESS;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean openSession(@UserIdInt int userId, @InstallFlags int installFlags,
String installerName, int installLocation,
@Nullable String originatingPackage, @Nullable Uri originatingUri,
int installScenario, int packageSource,
boolean requestUpdateOwnership, boolean disableVerification) {
// Changing package installer in stock Huawei with UID 2000 does not work
boolean canChangeInstaller = mHasInstallPackagePermission && (!HuaweiUtils.isStockHuawei() || Users.getSelfOrRemoteUid() != Ops.SHELL_UID);
String requestedInstallerPackageName = canChangeInstaller ? installerName : null;
String installerPackageName = Build.VERSION.SDK_INT < Build.VERSION_CODES.P && canChangeInstaller
? installerName : BuildConfig.APPLICATION_ID;
try {
mPackageInstaller = PackageManagerCompat.getPackageInstaller();
} catch (RemoteException e) {
callFinish(STATUS_FAILURE_SESSION_CREATE);
Log.e(TAG, "OpenSession: Could not get PackageInstaller.", e);
return false;
}
// Clean old sessions
cleanOldSessions();
// Create install session
SessionParams sessionParams = new SessionParams(SessionParams.MODE_FULL_INSTALL);
if (disableVerification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& SelfPermissions.isSystemOrRootOrShell()) {
// This disables verification for this UID temporarily
ExUtils.exceptionAsIgnored(() ->
mPackageInstaller.disableVerificationForUid(Users.getSelfOrRemoteUid()));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
installFlags |= INSTALL_DISABLE_VERIFICATION;
}
// In addition, we may also want to use the traditional methods
if (SelfPermissions.isShell()) {
mLastVerifyAdbInstallsResult = Settings.Global.getInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 1);
if (mLastVerifyAdbInstallsResult != 0) {
Settings.Global.putInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 0);
}
}
}
Refine.unsafeCast(sessionParams).installFlags |= installFlags;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Refine.unsafeCast(sessionParams).installerPackageName = requestedInstallerPackageName;
}
// Set installation location
sessionParams.setInstallLocation(installLocation);
// Set origin
if (originatingUri != null) {
sessionParams.setOriginatingUri(originatingUri);
}
if (originatingPackage != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
int uid = PackageUtils.getAppUid(new UserPackagePair(originatingPackage,
UserHandleHidden.myUserId()));
if (uid >= 0) {
sessionParams.setOriginatingUid(uid);
Log.d(TAG, "Setting originating uid: %d for %s", uid, originatingPackage);
}
}
// Set install reason
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sessionParams.setInstallReason(PackageManager.INSTALL_REASON_USER);
}
// Set install user action and install scenario
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// We hope system will not prompt an install confirmation
sessionParams.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
sessionParams.setInstallScenario(installScenario);
}
// Set package source (shell uses PACKAGE_SOURCE_OTHER)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
sessionParams.setPackageSource(packageSource);
}
// Set ownership (disable by default in shell)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
sessionParams.setApplicationEnabledSettingPersistent();
sessionParams.setRequestUpdateOwnership(requestUpdateOwnership);
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mSessionId = mPackageInstaller.createSession(sessionParams, installerPackageName, null, userId);
} else {
//noinspection deprecation
mSessionId = mPackageInstaller.createSession(sessionParams, installerPackageName, userId);
}
Log.d(TAG, "OpenSession: session id %d", mSessionId);
} catch (RemoteException e) {
callFinish(STATUS_FAILURE_SESSION_CREATE);
Log.e(TAG, "OpenSession: Failed to create install session.", e);
return false;
}
try {
mSession = Refine.unsafeCast(new PackageInstallerHidden.Session(IPackageInstallerSession.Stub.asInterface(
new ProxyBinder(mPackageInstaller.openSession(mSessionId).asBinder()))));
Log.d(TAG, "OpenSession: session opened.");
} catch (RemoteException e) {
callFinish(STATUS_FAILURE_SESSION_CREATE);
Log.e(TAG, "OpenSession: Failed to open install session.", e);
return false;
}
sendStartedBroadcast(mPackageName, mSessionId);
return true;
}
private void restoreVerifySettings() {
if (mLastVerifyAdbInstallsResult == 1) {
int val = Settings.Global.getInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 1);
if (val != 1) {
// Restore value
Settings.Global.putInt(mContext.getContentResolver(), SETTINGS_VERIFIER_VERIFY_ADB_INSTALLS, 1);
}
}
}
@InstallFlags
private static int getInstallFlags(@UserIdInt int userId) {
int flags = INSTALL_FROM_ADB | INSTALL_ALLOW_TEST | INSTALL_REPLACE_EXISTING;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
flags |= INSTALL_FULL_APP;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
flags |= INSTALL_REQUEST_DOWNGRADE | INSTALL_ALLOW_DOWNGRADE_API29;
} else flags |= INSTALL_ALLOW_DOWNGRADE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
flags |= INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK;
}
if (userId == UserHandleHidden.USER_ALL) {
flags |= INSTALL_ALL_USERS;
}
return flags;
}
public boolean installExisting(@NonNull String packageName, @UserIdInt int userId) {
ThreadUtils.ensureWorkerThread();
mPackageName = Objects.requireNonNull(packageName);
if (mOnInstallListener != null) {
mOnInstallListener.onStartInstall(mSessionId, packageName);
}
mInstallWatcher = new CountDownLatch(0);
mInteractionWatcher = new CountDownLatch(0);
if (!SelfPermissions.canInstallExistingPackages()) {
installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission.");
Log.d(TAG, "InstallExisting: Requires INSTALL_PACKAGES permission.");
return false;
}
// User ID must be a real user
List userIdWithoutInstalledPkg = new ArrayList<>();
switch (userId) {
case UserHandleHidden.USER_ALL: {
int[] userIds = Users.getUsersIds();
for (int u : userIds) {
try {
PackageManagerCompat.getPackageInfo(packageName,
PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, u);
} catch (Throwable th) {
userIdWithoutInstalledPkg.add(u);
}
}
break;
}
case UserHandleHidden.USER_NULL:
installCompleted(mSessionId, STATUS_FAILURE_INVALID, null, "STATUS_FAILURE_INVALID: No user is selected.");
Log.d(TAG, "InstallExisting: No user is selected.");
return false;
default:
try {
PackageManagerCompat.getPackageInfo(packageName,
PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId);
installCompleted(mSessionId, STATUS_FAILURE_ABORTED, null, "STATUS_FAILURE_ABORTED: Already installed.");
Log.d(TAG, "InstallExisting: Already installed.");
return false;
} catch (Throwable th) {
userIdWithoutInstalledPkg.add(userId);
}
}
if (userIdWithoutInstalledPkg.isEmpty()) {
installCompleted(mSessionId, STATUS_FAILURE_INVALID, null, "STATUS_FAILURE_INVALID: Could not find a valid user to perform install-existing.");
Log.d(TAG, "InstallExisting: Could not find any valid user.");
return false;
}
int installFlags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
installFlags |= INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS;
}
int installReason;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
installReason = PackageManager.INSTALL_REASON_USER;
} else installReason = 0;
for (int u : userIdWithoutInstalledPkg) {
if (!SelfPermissions.checkCrossUserPermission(u, true)) {
installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission.");
Log.d(TAG, "InstallExisting: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions.");
return false;
}
try {
int res = PackageManagerCompat.installExistingPackageAsUser(packageName, u, installFlags, installReason, null);
if (res != 1 /* INSTALL_SUCCEEDED */) {
installCompleted(mSessionId, res, null, null);
Log.e(TAG, "InstallExisting: Install failed with code %d", res);
return false;
}
if (u != UserHandleHidden.myUserId()) {
BroadcastUtils.sendPackageAdded(ContextUtils.getContext(), new String[]{packageName});
}
} catch (Throwable th) {
installCompleted(mSessionId, STATUS_FAILURE_ABORTED, null, "STATUS_FAILURE_ABORTED: " + th.getMessage());
Log.e(TAG, "InstallExisting: Could not install package for user %s", th, u);
return false;
}
}
installCompleted(mSessionId, STATUS_SUCCESS, null, null);
return true;
}
@WorkerThread
private void copyObb(@NonNull ApkFile apkFile, @UserIdInt int userId) {
if (!apkFile.hasObb()) return;
boolean tmpCloseApkFile = mCloseApkFile;
// Disable closing apk file in case the installation is finished already.
mCloseApkFile = false;
try {
// Get writable OBB directory
Path writableObbDir = ApkUtils.getOrCreateObbDir(mPackageName, userId);
// Delete old files
for (Path oldFile : writableObbDir.listFiles()) {
oldFile.delete();
}
apkFile.extractObb(writableObbDir);
ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.obb_files_extracted_successfully));
} catch (Exception e) {
Log.e(TAG, e);
ThreadUtils.postOnMainThread(() -> UIUtils.displayLongToast(R.string.failed_to_extract_obb_files));
} finally {
if (mInstallWatcher.getCount() != 0) {
// Reset close apk file if the installation isn't completed
mCloseApkFile = tmpCloseApkFile;
} else {
// Install completed, close apk file if requested
if (tmpCloseApkFile) apkFile.close();
}
}
}
private void cleanOldSessions() {
if (Users.getSelfOrRemoteUid() != Process.myUid()) {
// Only clean sessions for this UID
return;
}
List sessionInfoList;
try {
sessionInfoList = mPackageInstaller.getMySessions(mContext.getPackageName(), UserHandleHidden.myUserId()).getList();
} catch (Throwable e) {
Log.w(TAG, "CleanOldSessions: Could not get previous sessions.", e);
return;
}
for (PackageInstaller.SessionInfo sessionInfo : sessionInfoList) {
try {
mPackageInstaller.abandonSession(sessionInfo.getSessionId());
} catch (Throwable e) {
Log.w(TAG, "CleanOldSessions: Unable to abandon session", e);
}
}
}
private boolean abandon() {
if (mSession != null) {
try {
mSession.close();
} catch (Exception e) { // RemoteException
Log.e(TAG, "Abandon: Failed to abandon session.");
}
}
return false;
}
private void callFinish(int result) {
sendCompletedBroadcast(mContext, mPackageName, result, mSessionId);
}
private void installCompleted(int sessionId,
int finalStatus,
@Nullable String blockingPackage,
@Nullable String statusMessage) {
ThreadUtils.ensureWorkerThread();
if (finalStatus == STATUS_FAILURE_ABORTED
&& mSessionId == sessionId
&& mOnInstallListener != null) {
boolean privileged = SelfPermissions.checkSelfPermission(Manifest.permission.INSTALL_PACKAGES);
// MIUI-begin: In MIUI 12.5 and 20.2.0, it might be required to try installing the APK files more than once.
if (!privileged
&& MiuiUtils.isActualMiuiVersionAtLeast("12.5", "20.2.0")
&& Objects.equals(statusMessage, "INSTALL_FAILED_ABORTED: Permission denied")
&& mAttempts <= 3) {
// Try once more
++mAttempts;
Log.i(TAG, "MIUI: Installation attempt no %d for package %s", mAttempts, mPackageName);
mInteractionWatcher.countDown();
mInstallWatcher.countDown();
// Remove old broadcast receivers
unregisterReceiver();
mOnInstallListener.onAnotherAttemptInMiui(mApkFile);
return;
}
// MIUI-end
// HyperOS-begin: In HyperOS 2.0, installer package needs to be altered
if (privileged
// TODO: 1/10/25 Check for HyperOS?
&& statusMessage != null
&& statusMessage.startsWith("INSTALL_FAILED_HYPEROS_ISOLATION_VIOLATION: ")
&& mAttempts <= 2) {
// Try a second time with installer set to shell
++mAttempts;
Log.i(TAG, "HyperOS: %s", statusMessage);
Log.i(TAG, "HyperOS: Second attempt for %s", mPackageName);
mInteractionWatcher.countDown();
mInstallWatcher.countDown();
// Remove old broadcast receivers
unregisterReceiver();
mOnInstallListener.onSecondAttemptInHyperOsWithoutInstaller(mApkFile);
return;
}
// HyperOS-end
}
// No need to check package name since it's been checked before
if (finalStatus == STATUS_FAILURE_SESSION_CREATE || (mSessionId == sessionId)) {
if (mOnInstallListener != null) {
mOnInstallListener.onFinishedInstall(sessionId, mPackageName, finalStatus,
blockingPackage, statusMessage);
}
if (mCloseApkFile && mApkFile != null) {
mApkFile.close();
}
mInteractionWatcher.countDown();
mInstallWatcher.countDown();
}
}
@SuppressWarnings("deprecation")
public boolean uninstall(String packageName, @UserIdInt int userId, boolean keepData) {
ThreadUtils.ensureWorkerThread();
boolean hasDeletePackagesPermission = SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES);
mPackageName = Objects.requireNonNull(packageName);
String callerPackageName = SelfPermissions.getCallingPackage(Users.getSelfOrRemoteUid());
initBroadcastReceiver();
try {
if (userId == UserHandleHidden.USER_ALL && Users.getAllUserIds().length > 1
&& !SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.INTERACT_ACROSS_USERS_FULL)) {
installCompleted(mSessionId, STATUS_FAILURE_BLOCKED, "android", "STATUS_FAILURE_BLOCKED: Insufficient permission.");
Log.d(TAG, "Uninstall: Requires INTERACT_ACROSS_USERS and INTERACT_ACROSS_USERS_FULL permissions.");
return false;
}
int flags;
try {
flags = getDeleteFlags(packageName, userId, keepData);
} catch (Exception e) {
callFinish(STATUS_FAILURE_SESSION_CREATE);
Log.e(TAG, "Uninstall: Could not get PackageInstaller.", e);
return false;
}
userId = getCorrectUserIdForUninstallation(packageName, userId);
try {
mPackageInstaller = PackageManagerCompat.getPackageInstaller();
} catch (RemoteException e) {
callFinish(STATUS_FAILURE_SESSION_CREATE);
Log.e(TAG, "Uninstall: Could not get PackageInstaller.", e);
return false;
}
// Perform uninstallation
IntentSender sender;
LocalIntentReceiver intentReceiver;
if (hasDeletePackagesPermission) {
Log.d(TAG, "Uninstall: Uninstall via LocalIntentReceiver...");
try {
intentReceiver = new LocalIntentReceiver();
sender = intentReceiver.getIntentSender();
} catch (Exception e) {
callFinish(STATUS_FAILURE_SESSION_COMMIT);
Log.e(TAG, "Uninstall: Could not uninstall %s", e, packageName);
return false;
}
} else {
Log.d(TAG, "Uninstall: Calling activity to request permission...");
intentReceiver = null;
Intent callbackIntent = new Intent(PackageInstallerBroadcastReceiver.ACTION_PI_RECEIVER);
callbackIntent.setPackage(BuildConfig.APPLICATION_ID);
PendingIntent pendingIntent = PendingIntentCompat.getBroadcast(mContext, 0, callbackIntent, 0, true);
sender = pendingIntent.getIntentSender();
}
Log.d(TAG, "Uninstall: Uninstalling...");
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mPackageInstaller.uninstall(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
callerPackageName, flags, sender, userId);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mPackageInstaller.uninstall(packageName, callerPackageName, flags, sender, userId);
} else mPackageInstaller.uninstall(packageName, flags, sender, userId);
} catch (Throwable th) { // primarily RemoteException
callFinish(STATUS_FAILURE_SESSION_COMMIT);
Log.e(TAG, "Uninstall: Could not uninstall %s", th, packageName);
return false;
}
if (intentReceiver == null) {
Log.d(TAG, "Uninstall: Waiting for user interaction...");
// Wait for user interaction (if needed)
try {
// Wait for user interaction
mInteractionWatcher.await();
// Wait for the installation to complete
mInstallWatcher.await(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Log.e(TAG, "Installation interrupted.", e);
}
} else {
Intent resultIntent = intentReceiver.getResult();
mFinalStatus = resultIntent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
mStatusMessage = resultIntent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
}
Log.d(TAG, "Uninstall: Finished with status %d", mFinalStatus);
if (!mInstallCompleted) {
installCompleted(mSessionId, mFinalStatus, null, mStatusMessage);
}
if (mFinalStatus == PackageInstaller.STATUS_SUCCESS && userId != UserHandleHidden.myUserId()) {
BroadcastUtils.sendPackageAltered(ContextUtils.getContext(), new String[]{packageName});
}
return mFinalStatus == PackageInstaller.STATUS_SUCCESS;
} finally {
unregisterReceiver();
}
}
@DeleteFlags
private static int getDeleteFlags(@NonNull String packageName, @UserIdInt int userId, boolean keepData)
throws PackageManager.NameNotFoundException, RemoteException {
int flags = 0;
if (userId != UserHandleHidden.USER_ALL) {
PackageInfo info = PackageManagerCompat.getPackageInfo(packageName, MATCH_UNINSTALLED_PACKAGES
| PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, userId);
final boolean isSystem = (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
// If we are being asked to delete a system app for just one
// user set flag so it disables rather than reverting to system
// version of the app.
if (isSystem) {
flags |= DELETE_SYSTEM_APP;
}
} else {
flags |= DELETE_ALL_USERS;
}
if (keepData) {
flags |= DELETE_KEEP_DATA;
}
return flags;
}
private static int getCorrectUserIdForUninstallation(@NonNull String packageName, @UserIdInt int userId) {
if (userId == UserHandleHidden.USER_ALL) {
int[] users = Users.getAllUserIds();
for (int user : users) {
try {
PackageManagerCompat.getPackageInfo(packageName, MATCH_UNINSTALLED_PACKAGES
| PackageManagerCompat.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, user);
return user;
} catch (Throwable ignore) {
}
}
}
return userId;
}
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3855;drc=d31ee388115d17c2fd337f2806b37390c7d29834
private static class LocalIntentReceiver {
private final LinkedBlockingQueue mResult = new LinkedBlockingQueue<>();
private final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
@Override
public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission) {
send(intent);
return 0;
}
@Override
public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
send(intent);
return 0;
}
@Override
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
send(intent);
}
public void send(Intent intent) {
try {
mResult.offer(intent, 5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
@SuppressWarnings("JavaReflectionMemberAccess")
public IntentSender getIntentSender() throws Exception {
return IntentSender.class.getConstructor(IBinder.class)
.newInstance(mLocalSender.asBinder());
}
public Intent getResult() {
try {
return mResult.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private void unregisterReceiver() {
if (mPkgInstallerReceiver != null) {
ContextUtils.unregisterReceiver(mContext, mPkgInstallerReceiver);
}
ContextUtils.unregisterReceiver(mContext, mBroadcastReceiver);
}
private void initBroadcastReceiver() {
mInstallWatcher = new CountDownLatch(1);
mInteractionWatcher = new CountDownLatch(1);
mPkgInstallerReceiver = new PackageInstallerBroadcastReceiver();
mPkgInstallerReceiver.setAppLabel(mAppLabel);
mPkgInstallerReceiver.setPackageName(mPackageName);
ContextCompat.registerReceiver(mContext, mPkgInstallerReceiver,
new IntentFilter(PackageInstallerBroadcastReceiver.ACTION_PI_RECEIVER),
ContextCompat.RECEIVER_NOT_EXPORTED);
// Add receivers
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_INSTALL_COMPLETED);
intentFilter.addAction(ACTION_INSTALL_STARTED);
intentFilter.addAction(ACTION_INSTALL_INTERACTION_BEGIN);
intentFilter.addAction(ACTION_INSTALL_INTERACTION_END);
ContextCompat.registerReceiver(mContext, mBroadcastReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
}
private void sendStartedBroadcast(@NonNull String packageName, int sessionId) {
Intent broadcastIntent = new Intent(ACTION_INSTALL_STARTED);
broadcastIntent.setPackage(mContext.getPackageName());
broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
mContext.sendBroadcast(broadcastIntent);
}
static void sendCompletedBroadcast(@NonNull Context context, @NonNull String packageName, @Status int status,
int sessionId) {
Intent broadcastIntent = new Intent(ACTION_INSTALL_COMPLETED);
broadcastIntent.setPackage(context.getPackageName());
broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
broadcastIntent.putExtra(PackageInstaller.EXTRA_STATUS, status);
broadcastIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
context.sendBroadcast(broadcastIntent);
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerService.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.installer;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_ABORTED;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_BLOCKED;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_CONFLICT;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INCOMPATIBLE_ROM;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_INVALID;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SECURITY;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_ABANDON;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_COMMIT;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_CREATE;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_SESSION_WRITE;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_FAILURE_STORAGE;
import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.STATUS_SUCCESS;
import static io.github.muntashirakon.AppManager.history.ops.OpHistoryManager.HISTORY_TYPE_INSTALLER;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.PowerManager;
import android.os.UserHandleHidden;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.PendingIntentCompat;
import androidx.core.app.ServiceCompat;
import java.util.List;
import java.util.Objects;
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.apk.ApkFile;
import io.github.muntashirakon.AppManager.apk.ApkSource;
import io.github.muntashirakon.AppManager.apk.CachedApkSource;
import io.github.muntashirakon.AppManager.apk.dexopt.DexOptimizer;
import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
import io.github.muntashirakon.AppManager.history.ops.OpHistoryManager;
import io.github.muntashirakon.AppManager.intercept.IntentCompat;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.AppManager.main.MainActivity;
import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler;
import io.github.muntashirakon.AppManager.progress.NotificationProgressHandler.NotificationInfo;
import io.github.muntashirakon.AppManager.progress.ProgressHandler;
import io.github.muntashirakon.AppManager.progress.QueuedProgressHandler;
import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils;
import io.github.muntashirakon.AppManager.types.ForegroundService;
import io.github.muntashirakon.AppManager.types.UserPackagePair;
import io.github.muntashirakon.AppManager.utils.CpuUtils;
import io.github.muntashirakon.AppManager.utils.NotificationUtils;
import io.github.muntashirakon.AppManager.utils.PackageUtils;
import io.github.muntashirakon.AppManager.utils.ThreadUtils;
public class PackageInstallerService extends ForegroundService {
public static final String TAG = PackageInstallerService.class.getSimpleName();
public static final String EXTRA_QUEUE_ITEM = "queue_item";
public static final String CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INSTALL";
public interface OnInstallFinished {
@UiThread
void onFinished(String packageName, int status, @Nullable String blockingPackage,
@Nullable String statusMessage);
}
public PackageInstallerService() {
super(TAG);
}
@Nullable
private OnInstallFinished mOnInstallFinished;
private QueuedProgressHandler mProgressHandler;
private NotificationInfo mNotificationInfo;
private PowerManager.WakeLock mWakeLock;
@Override
public void onCreate() {
super.onCreate();
mWakeLock = CpuUtils.getPartialWakeLock("installer");
mWakeLock.acquire();
}
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
if (isWorking()) {
return super.onStartCommand(intent, flags, startId);
}
mProgressHandler = new NotificationProgressHandler(
this,
new NotificationProgressHandler.NotificationManagerInfo(CHANNEL_ID, "Install Progress", NotificationManagerCompat.IMPORTANCE_LOW),
NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO,
NotificationUtils.HIGH_PRIORITY_NOTIFICATION_INFO
);
mProgressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_PERCENT);
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntentCompat.getActivity(this, 0, notificationIntent, 0, false);
mNotificationInfo = new NotificationInfo()
.setBody(getString(R.string.install_in_progress))
.setOperationName(getText(R.string.package_installer))
.setDefaultAction(pendingIntent);
mProgressHandler.onAttach(this, mNotificationInfo);
return super.onStartCommand(intent, flags, startId);
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
ApkQueueItem apkQueueItem = getQueueItem(intent);
if (apkQueueItem == null) {
return;
}
InstallerOptions options = apkQueueItem.getInstallerOptions() != null
? apkQueueItem.getInstallerOptions()
: InstallerOptions.getDefault();
List selectedSplitIds = Objects.requireNonNull(apkQueueItem.getSelectedSplits());
// Install package
PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance();
installer.setAppLabel(apkQueueItem.getAppLabel());
installer.setOnInstallListener(new PackageInstallerCompat.OnInstallListener() {
@Override
public void onStartInstall(int sessionId, String packageName) {
}
// MIUI-begin: MIUI 12.5+ workaround
@Override
public void onAnotherAttemptInMiui(@Nullable ApkFile apkFile) {
if (apkFile != null) {
installer.install(apkFile, selectedSplitIds, options, mProgressHandler);
}
}
// MIUI-end
// HyperOS-begin: HyperOS 2.0+ workaround
@Override
public void onSecondAttemptInHyperOsWithoutInstaller(@Nullable ApkFile apkFile) {
if (apkFile != null) {
options.setInstallerName("com.android.shell");
installer.install(apkFile, selectedSplitIds, options, mProgressHandler);
}
}
// HyerOS-end
@Override
public void onFinishedInstall(int sessionId, String packageName, int result,
@Nullable String blockingPackage, @Nullable String statusMessage) {
boolean success = result == STATUS_SUCCESS;
OpHistoryManager.addHistoryItem(HISTORY_TYPE_INSTALLER, apkQueueItem, success);
if (success) {
// Block trackers if requested
if (options.isBlockTrackers()) {
ComponentUtils.blockTrackingComponents(new UserPackagePair(packageName, options.getUserId()));
}
// Perform force dex optimization if requested
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && options.isForceDexOpt()) {
// Ignore the result because it's irrelevant
new DexOptimizer(PackageManagerCompat.getPackageManager(), packageName).forceDexOpt();
}
}
finishInstallation(packageName, result, apkQueueItem.getAppLabel(), blockingPackage, statusMessage);
}
});
// Two possibilities: 1. Install-existing, 2. ApkFile/Uri
if (apkQueueItem.isInstallExisting()) {
// Install existing (need no progress)
String packageName = apkQueueItem.getPackageName();
if (packageName == null) {
// No package name supplied, abort
return;
}
installer.installExisting(packageName, options.getUserId());
} else {
// ApkFile/Uri
ApkSource apkSource = apkQueueItem.getApkSource();
if (apkSource == null) {
// No apk file, abort
return;
}
ApkFile apkFile;
try {
try {
apkFile = apkSource.resolve();
} catch (Throwable th) {
Log.w(TAG, "Could not get ApkFile", th);
OpHistoryManager.addHistoryItem(HISTORY_TYPE_INSTALLER, apkQueueItem, false);
String packageName = apkQueueItem.getPackageName();
finishInstallation(packageName != null ? packageName : "Unknown Package", STATUS_FAILURE_INVALID, apkQueueItem.getAppLabel(), null, null);
return;
}
installer.install(apkFile, selectedSplitIds, options, mProgressHandler);
} finally {
// Delete the cached file
if (apkSource instanceof CachedApkSource) {
((CachedApkSource) apkSource).cleanup();
}
}
}
}
@Override
protected void onQueued(@Nullable Intent intent) {
ApkQueueItem apkQueueItem = getQueueItem(intent);
String appLabel = apkQueueItem != null ? apkQueueItem.getAppLabel() : null;
Object notificationInfo = new NotificationInfo()
.setAutoCancel(true)
.setOperationName(getString(R.string.package_installer))
.setTitle(appLabel)
.setBody(getString(R.string.added_to_queue))
.setTime(System.currentTimeMillis());
mProgressHandler.onQueue(notificationInfo);
}
@Override
protected void onStartIntent(@Nullable Intent intent) {
// Set app name in the ongoing notification
ApkQueueItem apkQueueItem = getQueueItem(intent);
String appName;
if (apkQueueItem != null) {
String appLabel = apkQueueItem.getAppLabel();
appName = appLabel != null ? appLabel : apkQueueItem.getPackageName();
} else appName = null;
CharSequence title;
if (appName != null) {
title = getString(R.string.installing_package, appName);
} else {
title = getString(R.string.install_in_progress);
}
mNotificationInfo.setTitle(title);
mProgressHandler.onProgressStart(-1, 0, mNotificationInfo);
}
@Override
public void onDestroy() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
if (mProgressHandler != null) {
mProgressHandler.onDetach(this);
}
CpuUtils.releaseWakeLock(mWakeLock);
super.onDestroy();
}
public void setOnInstallFinished(@Nullable OnInstallFinished onInstallFinished) {
this.mOnInstallFinished = onInstallFinished;
}
@Nullable
private ApkQueueItem getQueueItem(@Nullable Intent intent) {
if (intent == null) {
return null;
}
return IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_QUEUE_ITEM, ApkQueueItem.class);
}
private void finishInstallation(@NonNull String packageName, int status,
@Nullable String appLabel, @Nullable String blockingPackage,
@Nullable String statusMessage) {
if (mOnInstallFinished != null) {
ThreadUtils.postOnMainThread(() -> {
if (mOnInstallFinished != null) {
mOnInstallFinished.onFinished(packageName, status, blockingPackage, statusMessage);
}
});
} else {
sendNotification(packageName, status, appLabel, blockingPackage, statusMessage);
}
}
private void sendNotification(@NonNull String packageName,
@PackageInstallerCompat.Status int status,
@Nullable String appLabel,
@Nullable String blockingPackage,
@Nullable String statusMessage) {
Intent intent = PackageManagerCompat.getLaunchIntentForPackage(packageName, UserHandleHidden.myUserId());
PendingIntent defaultAction = intent != null ? PendingIntentCompat.getActivity(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT, false) : null;
String subject = getStringFromStatus(this, status, appLabel, blockingPackage);
NotificationCompat.Style content = statusMessage != null ? new NotificationCompat.BigTextStyle()
.bigText(subject + "\n\n" + statusMessage) : null;
Object notificationInfo = new NotificationInfo()
.setAutoCancel(true)
.setTime(System.currentTimeMillis())
.setOperationName(getText(R.string.package_installer))
.setTitle(appLabel)
.setBody(subject)
.setStyle(content)
.setDefaultAction(defaultAction);
NotificationInfo progressNotificationInfo = (NotificationInfo) mProgressHandler.getLastMessage();
if (progressNotificationInfo != null) {
progressNotificationInfo.setBody(getString(R.string.done));
}
mProgressHandler.setProgressTextInterface(null);
ThreadUtils.postOnMainThread(() -> mProgressHandler.onResult(notificationInfo));
}
@NonNull
public static String getStringFromStatus(@NonNull Context context,
@PackageInstallerCompat.Status int status,
@Nullable CharSequence appLabel,
@Nullable String blockingPackage) {
switch (status) {
case STATUS_SUCCESS:
return context.getString(R.string.package_name_is_installed_successfully, appLabel);
case STATUS_FAILURE_ABORTED:
return context.getString(R.string.installer_error_aborted);
case STATUS_FAILURE_BLOCKED:
String blocker = context.getString(R.string.installer_error_blocked_device);
if (blockingPackage != null) {
blocker = PackageUtils.getPackageLabel(context.getPackageManager(), blockingPackage);
}
return context.getString(R.string.installer_error_blocked, blocker);
case STATUS_FAILURE_CONFLICT:
return context.getString(R.string.installer_error_conflict);
case STATUS_FAILURE_INCOMPATIBLE:
return context.getString(R.string.installer_error_incompatible);
case STATUS_FAILURE_INVALID:
return context.getString(R.string.installer_error_bad_apks);
case STATUS_FAILURE_STORAGE:
return context.getString(R.string.installer_error_storage);
case STATUS_FAILURE_SECURITY:
return context.getString(R.string.installer_error_security);
case STATUS_FAILURE_SESSION_CREATE:
return context.getString(R.string.installer_error_session_create);
case STATUS_FAILURE_SESSION_WRITE:
return context.getString(R.string.installer_error_session_write);
case STATUS_FAILURE_SESSION_COMMIT:
return context.getString(R.string.installer_error_session_commit);
case STATUS_FAILURE_SESSION_ABANDON:
return context.getString(R.string.installer_error_session_abandon);
case STATUS_FAILURE_INCOMPATIBLE_ROM:
return context.getString(R.string.installer_error_lidl_rom);
}
return context.getString(R.string.installer_error_generic);
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerViewModel.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.installer;
import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES;
import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.GET_SIGNING_CERTIFICATES_APK;
import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_DISABLED_COMPONENTS;
import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandleHidden;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Future;
import io.github.muntashirakon.AppManager.apk.ApkFile;
import io.github.muntashirakon.AppManager.apk.ApkSource;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils;
import io.github.muntashirakon.AppManager.utils.PackageUtils;
import io.github.muntashirakon.AppManager.utils.ThreadUtils;
import io.github.muntashirakon.io.IoUtils;
public class PackageInstallerViewModel extends AndroidViewModel {
private final PackageManager mPm;
private PackageInfo mNewPackageInfo;
private PackageInfo mInstalledPackageInfo;
private ApkSource mApkSource;
private ApkFile mApkFile;
private String mPackageName;
private String mAppLabel;
private Drawable mAppIcon;
private boolean mIsSignatureDifferent = false;
private int mTrackerCount;
@Nullable
private Future> mPackageInfoResult;
private final MutableLiveData mPackageInfoLiveData = new MutableLiveData<>();
private final MutableLiveData mPackageUninstalledLiveData = new MutableLiveData<>();
private final Set mSelectedSplits = new HashSet<>();
public PackageInstallerViewModel(@NonNull Application application) {
super(application);
mPm = application.getPackageManager();
}
@Override
protected void onCleared() {
IoUtils.closeQuietly(mApkFile);
if (mPackageInfoResult != null) {
mPackageInfoResult.cancel(true);
}
super.onCleared();
}
public LiveData packageInfoLiveData() {
return mPackageInfoLiveData;
}
public LiveData packageUninstalledLiveData() {
return mPackageUninstalledLiveData;
}
@AnyThread
public void getPackageInfo(ApkQueueItem apkQueueItem) {
if (mPackageInfoResult != null) {
mPackageInfoResult.cancel(true);
}
mSelectedSplits.clear();
mPackageInfoResult = ThreadUtils.postOnBackgroundThread(() -> {
try {
// Three possibilities: 1. Install-existing, 2. ApkFile, 3. Uri
if (apkQueueItem.isInstallExisting()) {
if (apkQueueItem.getPackageName() == null) {
throw new IllegalArgumentException("Package name not set for install-existing.");
}
getExistingPackageInfoInternal(apkQueueItem.getPackageName());
} else if (apkQueueItem.getApkSource() != null) {
mApkSource = apkQueueItem.getApkSource();
getPackageInfoInternal();
} else {
throw new IllegalArgumentException("Invalid queue item.");
}
apkQueueItem.setApkSource(mApkSource);
apkQueueItem.setPackageName(mPackageName);
apkQueueItem.setAppLabel(mAppLabel);
} catch (Throwable th) {
Log.e("PIVM", "Couldn't fetch package info", th);
mPackageInfoLiveData.postValue(null);
}
});
}
public void uninstallPackage() {
ThreadUtils.postOnBackgroundThread(() -> {
PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance();
installer.setAppLabel(mAppLabel);
mPackageUninstalledLiveData.postValue(installer.uninstall(mPackageName, UserHandleHidden.USER_ALL, false));
});
}
public PackageInfo getNewPackageInfo() {
return mNewPackageInfo;
}
@Nullable
public PackageInfo getInstalledPackageInfo() {
return mInstalledPackageInfo;
}
public String getAppLabel() {
return mAppLabel;
}
public Drawable getAppIcon() {
return mAppIcon;
}
public String getPackageName() {
return mPackageName;
}
public ApkFile getApkFile() {
return mApkFile;
}
public ApkSource getApkSource() {
return mApkSource;
}
public int getTrackerCount() {
return mTrackerCount;
}
public boolean isSignatureDifferent() {
return mIsSignatureDifferent;
}
public Set getSelectedSplits() {
return mSelectedSplits;
}
@NonNull
public ArrayList getSelectedSplitsForInstallation() {
if (mApkFile.isSplit()) {
if (mSelectedSplits.isEmpty()) {
throw new IllegalArgumentException("No splits selected.");
}
return new ArrayList<>(mSelectedSplits);
}
return new ArrayList<>(Collections.singletonList(mApkFile.getBaseEntry().id));
}
private void getPackageInfoInternal() throws PackageManager.NameNotFoundException, IOException, ApkFile.ApkFileException {
mApkFile = mApkSource.resolve();
mNewPackageInfo = loadNewPackageInfo();
mPackageName = mNewPackageInfo.packageName;
if (ThreadUtils.isInterrupted()) {
return;
}
try {
mInstalledPackageInfo = loadInstalledPackageInfo(mPackageName);
if (ThreadUtils.isInterrupted()) {
return;
}
} catch (PackageManager.NameNotFoundException ignore) {
}
mAppLabel = mPm.getApplicationLabel(mNewPackageInfo.applicationInfo).toString();
mAppIcon = mPm.getApplicationIcon(mNewPackageInfo.applicationInfo);
mTrackerCount = ComponentUtils.getTrackerComponentsCountForPackage(mNewPackageInfo);
if (ThreadUtils.isInterrupted()) {
return;
}
if (mNewPackageInfo != null && mInstalledPackageInfo != null) {
mIsSignatureDifferent = PackageUtils.isSignatureDifferent(mNewPackageInfo, mInstalledPackageInfo);
}
mPackageInfoLiveData.postValue(mNewPackageInfo);
}
private void getExistingPackageInfoInternal(@NonNull String packageName) throws PackageManager.NameNotFoundException, IOException, ApkFile.ApkFileException {
mPackageName = packageName;
mInstalledPackageInfo = loadInstalledPackageInfo(packageName);
mApkSource = ApkSource.getApkSource(mInstalledPackageInfo.applicationInfo);
mApkFile = mApkSource.resolve();
mNewPackageInfo = loadNewPackageInfo();
mAppLabel = mPm.getApplicationLabel(mNewPackageInfo.applicationInfo).toString();
mAppIcon = mPm.getApplicationIcon(mNewPackageInfo.applicationInfo);
mTrackerCount = ComponentUtils.getTrackerComponentsCountForPackage(mNewPackageInfo);
if (mNewPackageInfo != null && mInstalledPackageInfo != null) {
mIsSignatureDifferent = PackageUtils.isSignatureDifferent(mNewPackageInfo, mInstalledPackageInfo);
}
mPackageInfoLiveData.postValue(mNewPackageInfo);
}
@WorkerThread
@NonNull
private PackageInfo loadNewPackageInfo() throws PackageManager.NameNotFoundException, IOException {
String apkPath = mApkFile.getBaseEntry().getFile(false).getAbsolutePath();
int flags = PackageManager.GET_PERMISSIONS
| PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS
| PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | GET_SIGNING_CERTIFICATES_APK
| PackageManager.GET_CONFIGURATIONS | PackageManager.GET_SHARED_LIBRARY_FILES;
PackageInfo packageInfo = mPm.getPackageArchiveInfo(apkPath, flags);
if (packageInfo == null) {
// Previous method could return null if the APK isn't signed. So, try without it.
packageInfo = mPm.getPackageArchiveInfo(apkPath, flags & ~GET_SIGNING_CERTIFICATES_APK);
}
if (packageInfo == null) {
throw new PackageManager.NameNotFoundException("Package cannot be parsed.");
}
packageInfo.applicationInfo.sourceDir = apkPath;
packageInfo.applicationInfo.publicSourceDir = apkPath;
return packageInfo;
}
@WorkerThread
@NonNull
private PackageInfo loadInstalledPackageInfo(String packageName) throws PackageManager.NameNotFoundException {
@SuppressLint("WrongConstant")
PackageInfo packageInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS
| PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS
| PackageManager.GET_SERVICES | MATCH_DISABLED_COMPONENTS | GET_SIGNING_CERTIFICATES | MATCH_UNINSTALLED_PACKAGES
| PackageManager.GET_CONFIGURATIONS | PackageManager.GET_SHARED_LIBRARY_FILES);
if (packageInfo == null) throw new PackageManager.NameNotFoundException("Package not found.");
return packageInfo;
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/SupportedAppStores.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.installer;
import androidx.annotation.NonNull;
import java.util.HashMap;
public final class SupportedAppStores {
public static HashMap SUPPORTED_APP_STORES =
new HashMap() {{
// Sorted by app label
put("com.aurora.store", "Aurora Store");
put("com.looker.droidify", "Droid-ify");
put("org.fdroid.fdroid", "F-Droid");
put("org.fdroid.basic", "F-Droid Basic");
put("eu.bubu1.fdroidclassic", "F-Droid Classic");
put("com.machiav3lli.fdroid", "Neo Store");
}};
public static boolean isAppStoreSupported(@NonNull String packageName) {
return SUPPORTED_APP_STORES.containsKey(packageName);
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/list/AppListItem.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.list;
import android.graphics.Bitmap;
public class AppListItem {
public final String packageName;
private Bitmap mIcon;
private String mPackageLabel;
private long mVersionCode;
private String mVersionName;
private int mMinSdk;
private int mTargetSdk;
private String mSignatureSha256;
private long mFirstInstallTime;
private long mLastUpdateTime;
private String mInstallerPackageName;
private String mInstallerPackageLabel;
public AppListItem(String packageName) {
this.packageName = packageName;
}
public Bitmap getIcon() {
return mIcon;
}
public void setIcon(Bitmap icon) {
mIcon = icon;
}
public String getPackageLabel() {
return mPackageLabel;
}
public void setPackageLabel(String packageLabel) {
mPackageLabel = packageLabel;
}
public long getVersionCode() {
return mVersionCode;
}
public void setVersionCode(long versionCode) {
mVersionCode = versionCode;
}
public String getVersionName() {
return mVersionName;
}
public void setVersionName(String versionName) {
mVersionName = versionName;
}
public int getMinSdk() {
return mMinSdk;
}
public void setMinSdk(int minSdk) {
mMinSdk = minSdk;
}
public int getTargetSdk() {
return mTargetSdk;
}
public void setTargetSdk(int targetSdk) {
mTargetSdk = targetSdk;
}
public String getSignatureSha256() {
return mSignatureSha256;
}
public void setSignatureSha256(String signatureSha256) {
mSignatureSha256 = signatureSha256;
}
public long getFirstInstallTime() {
return mFirstInstallTime;
}
public void setFirstInstallTime(long firstInstallTime) {
mFirstInstallTime = firstInstallTime;
}
public long getLastUpdateTime() {
return mLastUpdateTime;
}
public void setLastUpdateTime(long lastUpdateTime) {
mLastUpdateTime = lastUpdateTime;
}
public String getInstallerPackageName() {
return mInstallerPackageName;
}
public void setInstallerPackageName(String installerPackageName) {
mInstallerPackageName = installerPackageName;
}
public String getInstallerPackageLabel() {
return mInstallerPackageLabel;
}
public void setInstallerPackageLabel(String installerPackageLabel) {
mInstallerPackageLabel = installerPackageLabel;
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/list/ListExporter.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.list;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.UserHandleHidden;
import android.text.TextUtils;
import android.util.Xml;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.core.content.pm.PackageInfoCompat;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
import io.github.muntashirakon.AppManager.utils.DateUtils;
import io.github.muntashirakon.AppManager.utils.ExUtils;
import io.github.muntashirakon.AppManager.utils.PackageUtils;
import io.github.muntashirakon.AppManager.utils.UIUtils;
import io.github.muntashirakon.csv.CsvWriter;
public final class ListExporter {
public static final int EXPORT_TYPE_CSV = 0;
public static final int EXPORT_TYPE_JSON = 1;
public static final int EXPORT_TYPE_XML = 2;
public static final int EXPORT_TYPE_MARKDOWN = 3;
@IntDef({EXPORT_TYPE_CSV, EXPORT_TYPE_JSON, EXPORT_TYPE_XML, EXPORT_TYPE_MARKDOWN})
@Retention(RetentionPolicy.SOURCE)
public @interface ExportType {
}
public static void export(@NonNull Context context,
@NonNull Writer writer,
@ExportType int exportType,
@NonNull List packageInfoList) throws IOException {
List appListItems = getAppListItems(context, packageInfoList);
switch (exportType) {
case EXPORT_TYPE_CSV:
exportCsv(writer, appListItems);
return;
case EXPORT_TYPE_JSON:
try {
exportJson(writer, appListItems);
} catch (JSONException e) {
ExUtils.rethrowAsIOException(e);
}
return;
case EXPORT_TYPE_XML:
exportXml(writer, appListItems);
return;
case EXPORT_TYPE_MARKDOWN:
exportMarkdown(context, writer, appListItems);
return;
}
throw new IllegalArgumentException("Invalid export type: " + exportType);
}
private static void exportXml(@NonNull Writer writer,
@NonNull List appListItems) throws IOException {
XmlSerializer xmlSerializer = Xml.newSerializer();
xmlSerializer.setOutput(writer);
xmlSerializer.startDocument("UTF-8", true);
xmlSerializer.docdecl("packages SYSTEM \"https://raw.githubusercontent.com/MuntashirAkon/AppManager/master/schema/packages.dtd\"");
xmlSerializer.startTag("", "packages");
xmlSerializer.attribute("", "version", String.valueOf(1));
for (AppListItem appListItem : appListItems) {
xmlSerializer.startTag("", "package");
xmlSerializer.attribute("", "name", appListItem.packageName);
xmlSerializer.attribute("", "label", appListItem.getPackageLabel());
xmlSerializer.attribute("", "versionCode", String.valueOf(appListItem.getVersionCode()));
xmlSerializer.attribute("", "versionName", appListItem.getVersionName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
xmlSerializer.attribute("", "minSdk", String.valueOf(appListItem.getMinSdk()));
}
xmlSerializer.attribute("", "targetSdk", String.valueOf(appListItem.getTargetSdk()));
xmlSerializer.attribute("", "signature", appListItem.getSignatureSha256());
xmlSerializer.attribute("", "firstInstallTime", String.valueOf(appListItem.getFirstInstallTime()));
xmlSerializer.attribute("", "lastUpdateTime", String.valueOf(appListItem.getLastUpdateTime()));
if (appListItem.getInstallerPackageName() != null) {
xmlSerializer.attribute("", "installerPackageName", appListItem.getInstallerPackageName());
if (appListItem.getInstallerPackageLabel() != null) {
xmlSerializer.attribute("", "installerPackageLabel", appListItem.getInstallerPackageLabel());
}
}
xmlSerializer.endTag("", "package");
}
xmlSerializer.endTag("", "packages");
xmlSerializer.endDocument();
xmlSerializer.flush();
}
private static void exportCsv(@NonNull Writer writer,
@NonNull List appListItems) throws IOException {
CsvWriter csvWriter = new CsvWriter(writer);
// Add header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
csvWriter.addLine(new String[]{"name", "label", "versionCode", "versionName", "minSdk",
"targetSdk", "signature", "firstInstallTime", "lastUpdateTime",
"installerPackageName", "installerPackageLabel"});
} else {
csvWriter.addLine(new String[]{"name", "label", "versionCode", "versionName",
"targetSdk", "signature", "firstInstallTime", "lastUpdateTime",
"installerPackageName", "installerPackageLabel"});
}
for (AppListItem item : appListItems) {
String installerPackage = item.getInstallerPackageName() != null ? item.getInstallerPackageName() : "";
String installerLabel = item.getInstallerPackageLabel() != null ? item.getInstallerPackageLabel() : "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
csvWriter.addLine(new String[]{item.packageName, item.getPackageLabel(),
String.valueOf(item.getVersionCode()), item.getVersionName(),
String.valueOf(item.getMinSdk()), String.valueOf(item.getTargetSdk()),
item.getSignatureSha256(), String.valueOf(item.getFirstInstallTime()),
String.valueOf(item.getLastUpdateTime()),
installerPackage, installerLabel});
} else {
csvWriter.addLine(new String[]{item.packageName, item.getPackageLabel(),
String.valueOf(item.getVersionCode()), item.getVersionName(),
String.valueOf(item.getTargetSdk()), item.getSignatureSha256(),
String.valueOf(item.getFirstInstallTime()),
String.valueOf(item.getLastUpdateTime()),
installerPackage, installerLabel});
}
}
}
private static void exportJson(@NonNull Writer writer,
@NonNull List appListItems)
throws JSONException, IOException {
// Should reflect packages.dtd
JSONArray array = new JSONArray();
for (AppListItem item : appListItems) {
JSONObject object = new JSONObject();
object.put("name", item.packageName);
object.put("label", item.getPackageLabel());
object.put("versionCode", item.getVersionCode());
object.put("versionName", item.getVersionName());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
object.put("minSdk", item.getMinSdk());
}
object.put("targetSdk", item.getTargetSdk());
object.put("signature", item.getSignatureSha256());
object.put("firstInstallTime", item.getFirstInstallTime());
object.put("lastUpdateTime", item.getLastUpdateTime());
if (item.getInstallerPackageName() != null) {
object.put("installerPackageName", item.getInstallerPackageName());
if (item.getInstallerPackageLabel() != null) {
object.put("installerPackageLabel", item.getInstallerPackageLabel());
}
}
array.put(object);
}
writer.write(array.toString(4));
}
private static void exportMarkdown(@NonNull Context context, @NonNull Writer writer,
@NonNull List appListItems) throws IOException {
writer.write("# Package Info\n\n");
for (AppListItem appListItem : appListItems) {
writer.append("## ").append(appListItem.getPackageLabel()).append("\n\n")
.append("**Package name:** ").append(appListItem.packageName).append("\n")
.append("**Version:** ").append(appListItem.getVersionName()).append(" (")
.append(String.valueOf(appListItem.getVersionCode())).append(")\n");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
writer.append("**Min SDK:** ").append(String.valueOf(appListItem.getMinSdk()))
.append(", ");
}
writer.append("**Target SDK:** ").append(String.valueOf(appListItem.getTargetSdk()))
.append("\n")
.append("**Date installed:** ")
.append(DateUtils.formatDateTime(context, appListItem.getFirstInstallTime()))
.append(", **Date updated:** ")
.append(DateUtils.formatDateTime(context, appListItem.getLastUpdateTime()))
.append("\n");
if (appListItem.getInstallerPackageName() != null) {
writer.append("**Installer:** ");
if (appListItem.getInstallerPackageLabel() != null) {
writer.append(appListItem.getInstallerPackageLabel()).append(" (");
}
writer.append(appListItem.getInstallerPackageName());
if (appListItem.getInstallerPackageLabel() != null) {
writer.append(")");
}
}
writer.append("\n\n");
}
}
@NonNull
private static List getAppListItems(@NonNull Context context,
@NonNull List packageInfoList) {
List appListItems = new ArrayList<>(packageInfoList.size());
PackageManager pm = context.getPackageManager();
for (PackageInfo packageInfo : packageInfoList) {
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
AppListItem item = new AppListItem(packageInfo.packageName);
appListItems.add(item);
item.setIcon(UIUtils.getBitmapFromDrawable(applicationInfo.loadIcon(pm)));
item.setPackageLabel(applicationInfo.loadLabel(pm).toString());
item.setVersionCode(PackageInfoCompat.getLongVersionCode(packageInfo));
item.setVersionName(packageInfo.versionName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
item.setMinSdk(applicationInfo.minSdkVersion);
}
item.setTargetSdk(applicationInfo.targetSdkVersion);
String[] signatureSha256 = PackageUtils.getSigningCertSha256Checksum(packageInfo, false);
item.setSignatureSha256(TextUtils.join(",", signatureSha256));
item.setFirstInstallTime(packageInfo.firstInstallTime);
item.setLastUpdateTime(packageInfo.lastUpdateTime);
String installerPackageName = PackageManagerCompat.getInstallerPackageName(
packageInfo.packageName, UserHandleHidden.getUserId(applicationInfo.uid));
if (installerPackageName != null) {
item.setInstallerPackageName(installerPackageName);
String installerPackageLabel;
try {
installerPackageLabel = pm.getApplicationInfo(installerPackageName, 0)
.loadLabel(pm).toString();
if (!installerPackageLabel.equals(installerPackageName)) {
item.setInstallerPackageLabel(installerPackageLabel);
}
} catch (PackageManager.NameNotFoundException ignore) {
}
}
}
return appListItems;
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/AndroidBinXmlDecoder.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.parser;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.reandroid.apk.AndroidFrameworks;
import com.reandroid.arsc.chunk.PackageBlock;
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
import com.reandroid.arsc.chunk.xml.ResXmlPullParser;
import com.reandroid.arsc.io.BlockReader;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import io.github.muntashirakon.AppManager.utils.IntegerUtils;
import io.github.muntashirakon.io.IoUtils;
public class AndroidBinXmlDecoder {
public static boolean isBinaryXml(@NonNull ByteBuffer buffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.mark();
int version = IntegerUtils.getUInt16(buffer);
int header = IntegerUtils.getUInt16(buffer);
buffer.reset();
// 0x0000 is NULL header. The only example of application using a NULL header is NP Manager
return (version == 0x0003 || version == 0x0000) && header == 0x0008;
}
@NonNull
public static String decode(@NonNull byte[] data) throws IOException {
return decode(ByteBuffer.wrap(data));
}
@NonNull
public static String decode(@NonNull InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] buf = new byte[IoUtils.DEFAULT_BUFFER_SIZE];
int n;
while (-1 != (n = is.read(buf))) {
buffer.write(buf, 0, n);
}
return decode(buffer.toByteArray());
}
@NonNull
public static String decode(@NonNull ByteBuffer byteBuffer) throws IOException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
decode(byteBuffer, bos);
byte[] bs = bos.toByteArray();
return new String(bs, StandardCharsets.UTF_8);
}
}
public static void decode(@NonNull ByteBuffer byteBuffer, @NonNull OutputStream os) throws IOException {
try (BlockReader reader = new BlockReader(byteBuffer.array());
PrintStream out = new PrintStream(os)) {
ResXmlDocument resXmlDocument = new ResXmlDocument();
resXmlDocument.readBytes(reader);
resXmlDocument.setPackageBlock(getFrameworkPackageBlock());
try (ResXmlPullParser parser = new ResXmlPullParser(resXmlDocument)) {
StringBuilder indent = new StringBuilder(10);
final String indentStep = " ";
out.println("");
XML_BUILDER:
while (true) {
int type = parser.next();
switch (type) {
case START_TAG: {
out.printf("%s<%s%s", indent, getNamespacePrefix(parser.getPrefix()), parser.getName());
indent.append(indentStep);
int nsStart = parser.getNamespaceCount(parser.getDepth() - 1);
int nsEnd = parser.getNamespaceCount(parser.getDepth());
for (int i = nsStart; i < nsEnd; ++i) {
out.printf("\n%sxmlns:%s=\"%s\"", indent,
parser.getNamespacePrefix(i),
parser.getNamespaceUri(i));
}
for (int i = 0; i != parser.getAttributeCount(); ++i) {
out.printf("\n%s%s%s=\"%s\"",
indent,
getNamespacePrefix(parser.getAttributePrefix(i)),
parser.getAttributeName(i),
parser.getAttributeValue(i));
}
out.println(">");
break;
}
case END_TAG: {
indent.setLength(indent.length() - indentStep.length());
out.printf("%s%s%s>%n", indent, getNamespacePrefix(parser.getPrefix()), parser.getName());
break;
}
case END_DOCUMENT:
break XML_BUILDER;
case START_DOCUMENT:
// Unreachable statement
break;
}
}
}
} catch (XmlPullParserException e) {
throw new IOException(e);
}
}
@NonNull
private static String getNamespacePrefix(String prefix) {
if (TextUtils.isEmpty(prefix)) {
return "";
}
return prefix + ":";
}
@NonNull
public static PackageBlock getFrameworkPackageBlock() {
if (sFrameworkPackageBlock != null) {
return sFrameworkPackageBlock;
}
sFrameworkPackageBlock = AndroidFrameworks.getLatest().getTableBlock().getAllPackages().next();
return sFrameworkPackageBlock;
}
private static PackageBlock sFrameworkPackageBlock;
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/AndroidBinXmlEncoder.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.parser;
import androidx.annotation.NonNull;
import com.reandroid.apk.xmlencoder.XMLEncodeSource;
import com.reandroid.xml.source.XMLFileParserSource;
import com.reandroid.xml.source.XMLParserSource;
import com.reandroid.xml.source.XMLStringParserSource;
import java.io.File;
import java.io.IOException;
public class AndroidBinXmlEncoder {
@NonNull
public static byte[] encodeFile(@NonNull File file) throws IOException {
return encode(new XMLFileParserSource(file.getName(), file));
}
@NonNull
public static byte[] encodeString(@NonNull String xml) throws IOException {
return encode(new XMLStringParserSource("String.xml", xml));
}
@NonNull
private static byte[] encode(@NonNull XMLParserSource xmlSource) throws IOException {
XMLEncodeSource xmlEncodeSource = new XMLEncodeSource(AndroidBinXmlDecoder.getFrameworkPackageBlock(), xmlSource);
return xmlEncodeSource.getBytes();
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/ManifestComponent.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.parser;
import android.content.ComponentName;
import java.util.ArrayList;
import java.util.List;
public class ManifestComponent {
public final ComponentName cn;
public final List intentFilters;
public ManifestComponent(ComponentName cn) {
this.cn = cn;
intentFilters = new ArrayList<>();
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/ManifestIntentFilter.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.parser;
import androidx.collection.ArraySet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class ManifestIntentFilter {
public final Set actions = new ArraySet<>();
public final Set categories = new ArraySet<>();
public final List data = new ArrayList<>();
public int priority;
public static class ManifestData {
public String scheme;
public String host;
public String port;
public String path;
public String pathPattern;
public String pathPrefix;
public String pathSuffix;
public String pathAdvancedPattern;
public String mimeType;
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/parser/ManifestParser.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.parser;
import android.content.ComponentName;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
import com.reandroid.arsc.chunk.xml.ResXmlElement;
import com.reandroid.arsc.io.BlockReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import io.github.muntashirakon.AppManager.logs.Log;
public class ManifestParser {
public static final String TAG = ManifestParser.class.getSimpleName();
// manifest
private static final String TAG_MANIFEST = "manifest";
private static final String ATTR_MANIFEST_PACKAGE = "package";
// manifest -> application
private static final String TAG_APPLICATION = "application";
// manifest -> application -> activity|activity-alias|service|receiver|provider
private static final String TAG_ACTIVITY = "activity";
private static final String TAG_ACTIVITY_ALIAS = "activity-alias";
private static final String TAG_SERVICE = "service";
private static final String TAG_RECEIVER = "receiver";
private static final String TAG_PROVIDER = "provider";
private static final String ATTR_NAME = "name"; // android:name
// manifest -> application -> (component) -> intent-filter
private static final String TAG_INTENT_FILTER = "intent-filter";
private static final String ATTR_PRIORITY = "priority"; // android:priority
// manifest -> application -> (component) -> intent-filter -> action|category|data
private static final String TAG_ACTION = "action";
private static final String TAG_CATEGORY = "category";
private static final String TAG_DATA = "data";
private final @NonNull ByteBuffer mManifestBytes;
private String mPackageName;
public ManifestParser(@NonNull byte[] manifestBytes) {
this(ByteBuffer.wrap(manifestBytes));
}
public ManifestParser(@NonNull ByteBuffer manifestBytes) {
mManifestBytes = manifestBytes;
}
public List parseComponents() throws IOException {
try (BlockReader reader = new BlockReader(mManifestBytes.array())) {
ResXmlDocument xmlBlock = new ResXmlDocument();
xmlBlock.readBytes(reader);
xmlBlock.setPackageBlock(AndroidBinXmlDecoder.getFrameworkPackageBlock());
ResXmlElement resManifestElement = xmlBlock.getDocumentElement();
// manifest
if (!TAG_MANIFEST.equals(resManifestElement.getName())) {
throw new IOException("\"manifest\" tag not found.");
}
String packageName = getAttributeValue(resManifestElement, ATTR_MANIFEST_PACKAGE);
if (packageName == null) {
throw new IOException("\"manifest\" does not have required attribute \"package\".");
}
mPackageName = packageName;
// manifest -> application
ResXmlElement resApplicationElement = null;
Iterator resXmlElementIt = resManifestElement.getElements(TAG_APPLICATION);
if (resXmlElementIt.hasNext()) {
resApplicationElement = resXmlElementIt.next();
}
if (resXmlElementIt.hasNext()) {
throw new IOException("\"manifest\" has duplicate \"application\" tags.");
}
if (resApplicationElement == null) {
Log.i(TAG, "package %s does not have \"application\" tag.", mPackageName);
return Collections.emptyList();
}
// manifest -> application -> component
List componentIfList = new ArrayList<>(resApplicationElement.getElementsCount());
String tagName;
resXmlElementIt = resApplicationElement.getElements();
while (resXmlElementIt.hasNext()) {
ResXmlElement elem = resXmlElementIt.next();
tagName = elem.getName();
if (tagName != null) {
switch (tagName) {
case TAG_ACTIVITY:
case TAG_ACTIVITY_ALIAS:
case TAG_SERVICE:
case TAG_RECEIVER:
case TAG_PROVIDER:
componentIfList.add(parseComponentInfo(elem));
break;
}
}
}
return componentIfList;
}
}
@NonNull
private ManifestComponent parseComponentInfo(@NonNull ResXmlElement componentElement) throws IOException {
String componentName = getAttributeValue(componentElement, ATTR_NAME);
if (componentName == null) {
throw new IOException("\"" + componentElement.getName() + "\" does not have required attribute \"android:name\".");
}
ManifestComponent componentIf = new ManifestComponent(new ComponentName(mPackageName, componentName));
// manifest -> application -> component -> intent-filter
Iterator resXmlElementIt = componentElement.getElements(TAG_INTENT_FILTER);
while (resXmlElementIt.hasNext()) {
ResXmlElement elem = resXmlElementIt.next();
componentIf.intentFilters.add(parseIntentFilter(elem));
}
return componentIf;
}
@NonNull
private ManifestIntentFilter parseIntentFilter(@NonNull ResXmlElement intentFilterElement) {
ManifestIntentFilter intentFilter = new ManifestIntentFilter();
String priorityString = getAttributeValue(intentFilterElement, ATTR_PRIORITY);
if (priorityString != null) {
intentFilter.priority = Integer.parseInt(priorityString);
}
// manifest -> application -> component -> intent-filter -> action|category|data
Iterator resXmlElementIt = intentFilterElement.getElements();
String tagName;
while (resXmlElementIt.hasNext()) {
ResXmlElement elem = resXmlElementIt.next();
tagName = elem.getName();
if (tagName != null) {
switch (tagName) {
case TAG_ACTION:
intentFilter.actions.add(Objects.requireNonNull(getAttributeValue(elem, ATTR_NAME)));
break;
case TAG_CATEGORY:
intentFilter.categories.add(Objects.requireNonNull(getAttributeValue(elem, ATTR_NAME)));
break;
case TAG_DATA:
intentFilter.data.add(parseData(elem));
break;
}
}
}
return intentFilter;
}
@NonNull
private ManifestIntentFilter.ManifestData parseData(@NonNull ResXmlElement dataElement) {
ManifestIntentFilter.ManifestData data = new ManifestIntentFilter.ManifestData();
ResXmlAttribute attribute;
for (int i = 0; i < dataElement.getAttributeCount(); ++i) {
attribute = dataElement.getAttributeAt(i);
if (attribute.equalsName("scheme")) {
data.scheme = attribute.getValueAsString();
} else if (attribute.equalsName("host")) {
data.host = attribute.getValueAsString();
} else if (attribute.equalsName("port")) {
data.port = attribute.getValueAsString();
} else if (attribute.equalsName("path")) {
data.path = attribute.getValueAsString();
} else if (attribute.equalsName("pathPrefix")) {
data.pathPrefix = attribute.getValueAsString();
} else if (attribute.equalsName("pathSuffix")) {
data.pathSuffix = attribute.getValueAsString();
} else if (attribute.equalsName("pathPattern")) {
data.pathPattern = attribute.getValueAsString();
} else if (attribute.equalsName("pathAdvancedPattern")) {
data.pathAdvancedPattern = attribute.getValueAsString();
} else if (attribute.equalsName("mimeType")) {
data.mimeType = attribute.getValueAsString();
} else {
Log.i(TAG, "Unknown intent-filter > data attribute %s", attribute.getName());
}
}
return data;
}
@Nullable
private String getAttributeValue(@NonNull ResXmlElement element, @NonNull String attrName) {
ResXmlAttribute attribute;
for (int i = 0; i < element.getAttributeCount(); ++i) {
attribute = element.getAttributeAt(i);
if (attribute.equalsName(attrName)) {
return attribute.getValueAsString();
}
}
return null;
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/SigSchemes.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.signing;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
public class SigSchemes {
@IntDef(flag = true, value = {
SIG_SCHEME_V1,
SIG_SCHEME_V2,
SIG_SCHEME_V3,
SIG_SCHEME_V4,
})
public @interface SignatureScheme {
}
public static final int SIG_SCHEME_V1 = 1 << 0;
public static final int SIG_SCHEME_V2 = 1 << 1;
public static final int SIG_SCHEME_V3 = 1 << 2;
public static final int SIG_SCHEME_V4 = 1 << 3;
public static final int TOTAL_SIG_SCHEME = 4;
public static final int DEFAULT_SCHEMES = SIG_SCHEME_V1 | SIG_SCHEME_V2;
@SignatureScheme
private int mFlags;
public SigSchemes(@SignatureScheme int flags) {
this.mFlags = flags;
}
public boolean isEmpty() {
return mFlags == 0;
}
public int getFlags() {
return mFlags;
}
public void setFlags(int flags) {
this.mFlags = flags;
}
@NonNull
public List getAllItems() {
List allItems = new ArrayList<>();
for (int i = 0; i < TOTAL_SIG_SCHEME; ++i) {
allItems.add(1 << i);
}
return allItems;
}
public boolean v1SchemeEnabled() {
return (mFlags & SIG_SCHEME_V1) != 0;
}
public boolean v2SchemeEnabled() {
return (mFlags & SIG_SCHEME_V2) != 0;
}
public boolean v3SchemeEnabled() {
return (mFlags & SIG_SCHEME_V3) != 0;
}
public boolean v4SchemeEnabled() {
return (mFlags & SIG_SCHEME_V4) != 0;
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/Signer.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.signing;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.apksig.ApkSigner;
import com.android.apksig.ApkVerifier;
import java.io.File;
import java.security.KeyStoreException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.Collections;
import java.util.List;
import aosp.libcore.util.HexEncoding;
import io.github.muntashirakon.AppManager.crypto.ks.KeyPair;
import io.github.muntashirakon.AppManager.crypto.ks.KeyStoreManager;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.AppManager.utils.DigestUtils;
import io.github.muntashirakon.AppManager.utils.ExUtils;
public class Signer {
public static final String TAG = "Signer";
public static final String SIGNING_KEY_ALIAS = "signing_key";
public static boolean canSign() {
try {
// In order to sign an APK, a signing key must be inserted
return KeyStoreManager.getInstance().containsKey(Signer.SIGNING_KEY_ALIAS);
} catch (Exception e) {
// Signing not configured
return false;
}
}
@NonNull
public static Signer getInstance(SigSchemes sigSchemes) throws SignatureException {
try {
KeyStoreManager manager = KeyStoreManager.getInstance();
KeyPair signingKey = manager.getKeyPair(SIGNING_KEY_ALIAS);
if (signingKey == null) {
throw new KeyStoreException("Alias " + SIGNING_KEY_ALIAS + " does not exist in KeyStore.");
}
return new Signer(sigSchemes, signingKey.getPrivateKey(), (X509Certificate) signingKey.getCertificate());
} catch (Exception e) {
throw new SignatureException(e);
}
}
@NonNull
private final PrivateKey mPrivateKey;
@NonNull
private final X509Certificate mCertificate;
@NonNull
private final SigSchemes mSigSchemes;
@Nullable
private File mIdsigFile;
private Signer(@NonNull SigSchemes sigSchemes, @NonNull PrivateKey privateKey, @NonNull X509Certificate certificate) {
mSigSchemes = sigSchemes;
mPrivateKey = privateKey;
mCertificate = certificate;
}
public boolean isV4SchemeEnabled() {
return mSigSchemes.v4SchemeEnabled();
}
public void setIdsigFile(@Nullable File idsigFile) {
mIdsigFile = idsigFile;
}
public boolean sign(File in, File out, int minSdk, boolean alignFileSize) {
ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder("CERT",
mPrivateKey, Collections.singletonList(mCertificate)).build();
ApkSigner.Builder builder = new ApkSigner.Builder(Collections.singletonList(signerConfig));
builder.setInputApk(in);
builder.setOutputApk(out);
builder.setCreatedBy("AppManager");
builder.setAlignFileSize(alignFileSize);
if (minSdk != -1) builder.setMinSdkVersion(minSdk);
builder.setV1SigningEnabled(mSigSchemes.v1SchemeEnabled());
builder.setV2SigningEnabled(mSigSchemes.v2SchemeEnabled());
builder.setV3SigningEnabled(mSigSchemes.v3SchemeEnabled());
if (mSigSchemes.v4SchemeEnabled()) {
if (mIdsigFile == null) {
throw new RuntimeException("idsig file is mandatory for v4 signature scheme.");
}
builder.setV4SigningEnabled(true);
builder.setV4SignatureOutputFile(mIdsigFile);
} else {
builder.setV4SigningEnabled(false);
}
ApkSigner signer = builder.build();
Log.i(TAG, "SignApk: %s", in);
try {
if (alignFileSize && !ZipAlign.verify(in, ZipAlign.ALIGNMENT_4, true)) {
ZipAlign.align(in, ZipAlign.ALIGNMENT_4, true);
}
signer.sign();
Log.i(TAG, "The signature is complete and the output file is %s", out);
return true;
} catch (Exception e) {
Log.w(TAG, e);
return false;
}
}
public static boolean verify(@NonNull SigSchemes sigSchemes, @NonNull File apk, @Nullable File idsig) {
ApkVerifier.Builder builder = new ApkVerifier.Builder(apk)
.setMaxCheckedPlatformVersion(Build.VERSION.SDK_INT);
if (sigSchemes.v4SchemeEnabled()) {
if (idsig == null) {
throw new RuntimeException("idsig file is mandatory for v4 signature scheme.");
}
builder.setV4SignatureFile(idsig);
}
ApkVerifier verifier = builder.build();
try {
ApkVerifier.Result result = verifier.verify();
Log.i(TAG, "%s", apk);
boolean isVerify = result.isVerified();
if (isVerify) {
if (sigSchemes.v1SchemeEnabled() && result.isVerifiedUsingV1Scheme()) {
Log.i(TAG, "V1 signature verified.");
} else Log.w(TAG, "V1 signature verification failed/disabled.");
if (sigSchemes.v2SchemeEnabled() && result.isVerifiedUsingV2Scheme()) {
Log.i(TAG, "V2 signature verified.");
} else Log.w(TAG, "V2 signature verification failed/disabled.");
if (sigSchemes.v3SchemeEnabled()) {
if (result.isVerifiedUsingV3Scheme()) {
Log.i(TAG, "V3 signature verified.");
} else Log.w(TAG, "V3 signature verification failed.");
if (result.isVerifiedUsingV31Scheme()) {
Log.i(TAG, "V3.1 signature verified.");
} else Log.w(TAG, "V3.1 signature verification failed.");
} else Log.w(TAG, "V3 signature verification disabled.");
if (sigSchemes.v4SchemeEnabled() && result.isVerifiedUsingV4Scheme()) {
Log.i(TAG, "V4 signature verified.");
} else Log.w(TAG, "V4 signature verification failed/disabled.");
if (result.isSourceStampVerified()) {
Log.i(TAG, "SourceStamp verified.");
} else Log.w(TAG, "SourceStamp not verified/unavailable.");
int i = 0;
List signerCertificates = result.getSignerCertificates();
Log.i(TAG, "Number of signatures: %d", signerCertificates.size());
for (X509Certificate logCert : signerCertificates) {
i++;
logCert(logCert, "Signature" + i);
}
}
for (ApkVerifier.IssueWithParams warn : result.getWarnings()) {
Log.w(TAG, "%s", warn);
}
for (ApkVerifier.IssueWithParams err : result.getErrors()) {
Log.e(TAG, "%s", err);
}
if (sigSchemes.v1SchemeEnabled()) {
for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeIgnoredSigners()) {
String name = signer.getName();
for (ApkVerifier.IssueWithParams err : signer.getErrors()) {
Log.e(TAG, "%s: %s", name, err);
}
for (ApkVerifier.IssueWithParams err : signer.getWarnings()) {
Log.w(TAG, "%s: %s", name, err);
}
}
}
return isVerify;
} catch (Exception e) {
Log.w(TAG, "Verification failed.", e);
return false;
}
}
@Nullable
public static String getSourceStampSource(@NonNull ApkVerifier.Result.SourceStampInfo sourceStampInfo) {
byte[] certBytes = ExUtils.exceptionAsNull(() -> sourceStampInfo.getCertificate().getEncoded());
if (certBytes == null) {
return null;
}
String sourceStampHash = DigestUtils.getHexDigest(DigestUtils.SHA_256, certBytes);
if (sourceStampHash.equals("3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d")) {
return "Google Play";
}
return null;
}
private static void logCert(@NonNull X509Certificate x509Certificate, CharSequence charSequence) throws CertificateEncodingException {
int bitLength;
Principal subjectDN = x509Certificate.getSubjectDN();
Log.i(TAG, "%s - Unique distinguished name: %s", charSequence, subjectDN);
logEncoded(charSequence, x509Certificate.getEncoded());
PublicKey publicKey = x509Certificate.getPublicKey();
if (publicKey instanceof RSAKey) {
bitLength = ((RSAKey) publicKey).getModulus().bitLength();
} else if (publicKey instanceof ECKey) {
bitLength = ((ECKey) publicKey).getParams().getOrder().bitLength();
} else if (publicKey instanceof DSAKey) {
DSAParams params = ((DSAKey) publicKey).getParams();
if (params != null) {
bitLength = params.getP().bitLength();
} else bitLength = -1;
} else {
bitLength = -1;
}
Log.i(TAG, "%s - key size: %s", charSequence, (bitLength != -1 ? String.valueOf(bitLength) : "Unknown"));
String algorithm = publicKey.getAlgorithm();
Log.i(TAG, "%s - key algorithm: %s", charSequence, algorithm);
logEncoded(charSequence, publicKey.getEncoded());
}
private static void logEncoded(CharSequence charSequence, byte[] bArr) {
log(charSequence + " - SHA-256: ", DigestUtils.getDigest(DigestUtils.SHA_256, bArr));
log(charSequence + " - SHA-1: ", DigestUtils.getDigest(DigestUtils.SHA_1, bArr));
log(charSequence + " - MD5: ", DigestUtils.getDigest(DigestUtils.MD5, bArr));
}
private static void log(String str, byte[] bArr) {
Log.i(TAG, str);
Log.w(TAG, HexEncoding.encodeToString(bArr));
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/SignerInfo.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.signing;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.apksig.ApkVerifier;
import com.android.apksig.SigningCertificateLineage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
public class SignerInfo {
@Nullable
private final X509Certificate[] mCurrentSignerCerts;
@Nullable
private final X509Certificate[] mSignerCertsInLineage;
@Nullable
private final X509Certificate[] mAllSignerCerts;
@Nullable
private final X509Certificate mSourceStampCert;
public SignerInfo(@NonNull ApkVerifier.Result apkVerifierResult) {
List certificates = apkVerifierResult.getSignerCertificates();
if (certificates == null || certificates.isEmpty()) {
mCurrentSignerCerts = null;
} else {
mCurrentSignerCerts = new X509Certificate[certificates.size()];
int i = 0;
for (X509Certificate certificate : certificates) {
mCurrentSignerCerts[i++] = certificate;
}
}
// Collect source stamp certificate
ApkVerifier.Result.SourceStampInfo sourceStampInfo = apkVerifierResult.getSourceStampInfo();
mSourceStampCert = sourceStampInfo != null ? sourceStampInfo.getCertificate() : null;
if (mCurrentSignerCerts == null || mCurrentSignerCerts.length > 1) {
// Skip checking rotation because the app has multiple signers or no signer at all
mAllSignerCerts = mCurrentSignerCerts;
mSignerCertsInLineage = null;
return;
}
SigningCertificateLineage lineage = apkVerifierResult.getSigningCertificateLineage();
if (lineage == null) {
// There is no SigningCertificateLineage block
mAllSignerCerts = mCurrentSignerCerts;
mSignerCertsInLineage = null;
return;
}
List certificatesInLineage = lineage.getCertificatesInLineage();
if (certificatesInLineage == null || certificatesInLineage.isEmpty()) {
// There is no certificate in the SigningCertificateLineage block
mAllSignerCerts = mCurrentSignerCerts;
mSignerCertsInLineage = null;
return;
}
// At this point, currentSignatures is a singleton array
mSignerCertsInLineage = certificatesInLineage.toArray(new X509Certificate[0]);
mAllSignerCerts = new X509Certificate[mCurrentSignerCerts.length + certificatesInLineage.size()];
int i = 0;
// Add the current signature on top
for (X509Certificate signature : mCurrentSignerCerts) {
mAllSignerCerts[i++] = signature;
}
for (X509Certificate certificate : certificatesInLineage) {
mAllSignerCerts[i++] = certificate;
}
}
@RequiresApi(Build.VERSION_CODES.P)
public SignerInfo(@Nullable SigningInfo signingInfo) {
mSourceStampCert = null;
if (signingInfo == null) {
mCurrentSignerCerts = null;
mSignerCertsInLineage = null;
mAllSignerCerts = null;
return;
}
Signature[] currentSignatures = signingInfo.getApkContentsSigners();
Signature[] lineageSignatures = signingInfo.getSigningCertificateHistory();
boolean isLineage = !signingInfo.hasMultipleSigners() && signingInfo.hasPastSigningCertificates();
// Validation
if (currentSignatures == null || currentSignatures.length == 0) {
// Invalid signatures
mCurrentSignerCerts = null;
mSignerCertsInLineage = null;
mAllSignerCerts = null;
return;
}
if (isLineage && (lineageSignatures == null || lineageSignatures.length == 0)) {
// Invalid lineage signatures
mCurrentSignerCerts = null;
mSignerCertsInLineage = null;
mAllSignerCerts = null;
return;
}
int totalSigner = currentSignatures.length + (isLineage ? lineageSignatures.length : 0);
mCurrentSignerCerts = new X509Certificate[currentSignatures.length];
mAllSignerCerts = new X509Certificate[totalSigner];
for (int i = 0; i < currentSignatures.length; ++i) {
X509Certificate cert = generateCertificateOrFail(currentSignatures[i]);
mCurrentSignerCerts[i] = cert;
mAllSignerCerts[i] = cert;
}
if (isLineage) {
mSignerCertsInLineage = new X509Certificate[lineageSignatures.length];
for (int i = currentSignatures.length, j = 0; i < totalSigner; ++i, ++j) {
X509Certificate cert = generateCertificateOrFail(lineageSignatures[j]);
mSignerCertsInLineage[j] = cert;
mAllSignerCerts[i] = cert;
}
} else mSignerCertsInLineage = null;
}
public SignerInfo(@Nullable Signature[] signatures) {
mSourceStampCert = null;
mSignerCertsInLineage = null;
if (signatures != null && signatures.length > 0) {
mAllSignerCerts = new X509Certificate[signatures.length];
mCurrentSignerCerts = new X509Certificate[signatures.length];
for (int i = 0; i < signatures.length; ++i) {
X509Certificate cert = generateCertificateOrFail(signatures[i]);
mAllSignerCerts[i] = cert;
mCurrentSignerCerts[i] = cert;
}
} else {
mCurrentSignerCerts = null;
mAllSignerCerts = null;
}
}
public boolean hasMultipleSigners() {
return mCurrentSignerCerts != null && mCurrentSignerCerts.length > 1;
}
public boolean hasProofOfRotation() {
return !hasMultipleSigners() && mSignerCertsInLineage != null;
}
@Nullable
public X509Certificate[] getCurrentSignerCerts() {
return mCurrentSignerCerts;
}
@Nullable
public X509Certificate getSourceStampCert() {
return mSourceStampCert;
}
@Nullable
public X509Certificate[] getSignerCertsInLineage() {
return mSignerCertsInLineage;
}
/**
* Retrieve all signatures, including the lineage ones. The current signature(s) are on top of the array.
*
* If the APK has multiple signers, all signatures are the current signatures, and if the APK has only one
* signer, the first signature is the current signature and rests are the lineage signature.
*/
@Nullable
public X509Certificate[] getAllSignerCerts() {
return mAllSignerCerts;
}
private static X509Certificate generateCertificateOrFail(Signature signature) {
try (InputStream is = new ByteArrayInputStream(signature.toByteArray())) {
return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
} catch (IOException | CertificateException e) {
throw new RuntimeException("Invalid signature", e);
}
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/signing/ZipAlign.java
================================================
// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.signing;
import androidx.annotation.NonNull;
import com.reandroid.archive.ArchiveEntry;
import com.reandroid.archive.ArchiveFile;
import com.reandroid.archive.writer.ApkFileWriter;
import com.reandroid.archive.writer.ZipAligner;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.io.Paths;
public class ZipAlign {
public static final String TAG = ZipAlign.class.getSimpleName();
public static final int ALIGNMENT_4 = 4;
private static final int ALIGNMENT_PAGE = 4096;
public static void align(@NonNull File input, @NonNull File output, int alignment, boolean pageAlignSharedLibs)
throws IOException {
File dir = output.getParentFile();
if (!Paths.exists(dir)) {
dir.mkdirs();
}
try (ArchiveFile archive = new ArchiveFile(input);
ApkFileWriter apkWriter = new ApkFileWriter(output, archive.getInputSources())) {
apkWriter.setZipAligner(getZipAligner(alignment, pageAlignSharedLibs));
apkWriter.write();
}
if (!verify(output, alignment, pageAlignSharedLibs)) {
throw new IOException("Could not verify aligned APK file.");
}
}
public static void align(@NonNull File inFile, int alignment, boolean pageAlignSharedLibs) throws IOException {
File tmp = toTmpFile(inFile);
tmp.delete();
try {
align(inFile, tmp, alignment, pageAlignSharedLibs);
inFile.delete();
tmp.renameTo(inFile);
} catch (IOException e) {
tmp.delete();
throw e;
}
}
public static boolean verify(@NonNull File file, int alignment, boolean pageAlignSharedLibs) {
ArchiveFile zipFile;
boolean foundBad = false;
Log.d(TAG, "Verifying alignment of %s...", file);
try {
zipFile = new ArchiveFile(file);
} catch (IOException e) {
Log.e(TAG, "Unable to open '%s' for verification", e, file);
return false;
}
Iterator entryIterator = zipFile.iterator();
while (entryIterator.hasNext()) {
ArchiveEntry pEntry = entryIterator.next();
String name = pEntry.getName();
long fileOffset = pEntry.getFileOffset();
if (pEntry.getMethod() == ZipEntry.DEFLATED) {
Log.d(TAG, "%8d %s (OK - compressed)", fileOffset, name);
} else if (pEntry.isDirectory()) {
// Directory entries do not need to be aligned.
Log.d(TAG, "%8d %s (OK - directory)", fileOffset, name);
} else {
int alignTo = getAlignment(pEntry, alignment, pageAlignSharedLibs);
if ((fileOffset % alignTo) != 0) {
Log.w(TAG, "%8d %s (BAD - %d)\n", fileOffset, name, (fileOffset % alignTo));
foundBad = true;
break;
} else {
Log.d(TAG, "%8d %s (OK)\n", fileOffset, name);
}
}
}
Log.d(TAG, "Verification %s\n", foundBad ? "FAILED" : "successful");
try {
zipFile.close();
} catch (IOException e) {
Log.w(TAG, "Unable to close '%s'", e, file);
}
return !foundBad;
}
private static int getAlignment(@NonNull ArchiveEntry entry, int defaultAlignment, boolean pageAlignSharedLibs) {
if (!pageAlignSharedLibs) {
return defaultAlignment;
}
String name = entry.getName();
if (name.startsWith("lib/") && name.endsWith(".so")) {
return ALIGNMENT_PAGE;
} else {
return defaultAlignment;
}
}
@NonNull
public static ZipAligner getZipAligner(int defaultAlignment, boolean pageAlignSharedLibs) {
ZipAligner zipAligner = new ZipAligner();
zipAligner.setDefaultAlignment(defaultAlignment);
if (pageAlignSharedLibs) {
Pattern patternNativeLib = Pattern.compile("^lib/.+\\.so$");
zipAligner.setFileAlignment(patternNativeLib, ALIGNMENT_PAGE);
}
return zipAligner;
}
@NonNull
private static File toTmpFile(@NonNull File file) {
String name = file.getName() + ".align.tmp";
File dir = file.getParentFile();
if (dir == null) {
return new File(name);
}
return new File(dir, name);
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/ApksMetadata.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.splitapk;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import androidx.core.content.pm.PackageInfoCompat;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipOutputStream;
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.apk.ApkUtils;
import io.github.muntashirakon.AppManager.logs.Log;
import io.github.muntashirakon.AppManager.self.filecache.FileCache;
import io.github.muntashirakon.AppManager.utils.ContextUtils;
import io.github.muntashirakon.AppManager.utils.JSONUtils;
import io.github.muntashirakon.io.Path;
import io.github.muntashirakon.io.Paths;
public class ApksMetadata {
public static final String TAG = ApksMetadata.class.getSimpleName();
public static final String META_FILE = "info.json";
public static final String ICON_FILE = "icon.png";
public static class Dependency {
public static final String DEPENDENCY_MATCH_EXACT = "exact";
public static final String DEPENDENCY_MATCH_GREATER = "greater";
public static final String DEPENDENCY_MATCH_LESS = "less";
@StringDef({DEPENDENCY_MATCH_EXACT, DEPENDENCY_MATCH_GREATER, DEPENDENCY_MATCH_LESS})
@Retention(RetentionPolicy.SOURCE)
public @interface DependencyMatch {
}
public String packageName;
public String displayName;
public String versionName;
public long versionCode;
@Nullable
public String[] signatures;
@DependencyMatch
public String match;
public boolean required;
@Nullable
public String path;
}
public static class BuildInfo {
public final long timestamp;
public final String builderId;
public final String builderLabel;
public final String builderVersion;
public final String platform;
public BuildInfo() {
timestamp = System.currentTimeMillis();
builderId = BuildConfig.APPLICATION_ID;
builderLabel = ContextUtils.getContext().getString(R.string.app_name);
builderVersion = BuildConfig.VERSION_NAME;
platform = "android";
}
public BuildInfo(long timestamp, String builderId, String builderLabel, String builderVersion, String platform) {
this.timestamp = timestamp;
this.builderId = builderId;
this.builderLabel = builderLabel;
this.builderVersion = builderVersion;
this.platform = platform;
}
}
public long exportTimestamp;
public long metaVersion = 1L;
public String packageName;
public String displayName;
public String versionName;
public long versionCode;
public long minSdk = 0L;
public long targetSdk;
public BuildInfo buildInfo;
public final List dependencies = new ArrayList<>();
private final PackageInfo mPackageInfo;
public ApksMetadata() {
mPackageInfo = null;
}
public ApksMetadata(PackageInfo packageInfo) {
mPackageInfo = packageInfo;
}
public void readMetadata(String jsonString) throws JSONException {
JSONObject jsonObject = new JSONObject(jsonString);
metaVersion = jsonObject.getLong("info_version");
packageName = jsonObject.getString("package_name");
displayName = jsonObject.getString("display_name");
versionName = jsonObject.getString("version_name");
versionCode = jsonObject.getLong("version_code");
minSdk = jsonObject.optLong("min_sdk", 0);
targetSdk = jsonObject.getLong("target_sdk");
// Build info
JSONObject buildInfoObject = jsonObject.optJSONObject("build_info");
if (buildInfoObject != null) {
buildInfo = new BuildInfo(buildInfoObject.getLong("timestamp"),
buildInfoObject.getString("builder_id"),
buildInfoObject.getString("builder_label"),
buildInfoObject.getString("builder_version"),
buildInfoObject.getString("platform"));
}
// Dependencies
JSONArray dependencyInfoArray = jsonObject.optJSONArray("dependencies");
if (dependencyInfoArray != null) {
for (int i = 0; i < dependencyInfoArray.length(); ++i) {
JSONObject dependencyInfoObject = dependencyInfoArray.getJSONObject(i);
Dependency dependency = new Dependency();
dependency.packageName = dependencyInfoObject.getString("package_name");
dependency.displayName = dependencyInfoObject.getString("display_name");
dependency.versionName = dependencyInfoObject.getString("version_name");
dependency.versionCode = dependencyInfoObject.getLong("version_code");
String signatures = JSONUtils.getString(dependencyInfoObject, "signature", null);
if (signatures != null) {
dependency.signatures = signatures.split(",");
}
dependency.match = dependencyInfoObject.getString("match");
dependency.required = dependencyInfoObject.getBoolean("required");
dependency.path = JSONUtils.getString(dependencyInfoObject, "path", null);
dependencies.add(dependency);
}
}
}
public void writeMetadata(@NonNull ZipOutputStream zipOutputStream) throws IOException {
// Fetch meta
PackageManager pm = ContextUtils.getContext().getPackageManager();
ApplicationInfo applicationInfo = mPackageInfo.applicationInfo;
packageName = mPackageInfo.packageName;
displayName = applicationInfo.loadLabel(pm).toString();
versionName = mPackageInfo.versionName;
versionCode = PackageInfoCompat.getLongVersionCode(mPackageInfo);
exportTimestamp = 946684800000L; // Fake time
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
minSdk = applicationInfo.minSdkVersion;
}
targetSdk = applicationInfo.targetSdkVersion;
String[] sharedLibraries = applicationInfo.sharedLibraryFiles;
if (sharedLibraries != null) {
for (String file : sharedLibraries) {
if (!file.endsWith(".apk")) {
continue;
}
PackageInfo packageInfo = pm.getPackageArchiveInfo(file, PackageManager.GET_SHARED_LIBRARY_FILES);
if (packageInfo == null) {
Log.w(TAG, "Could not fetch package info for file %s", file);
continue;
}
if (packageInfo.applicationInfo.sourceDir == null) {
packageInfo.applicationInfo.sourceDir = file;
}
if (packageInfo.applicationInfo.publicSourceDir == null) {
packageInfo.applicationInfo.publicSourceDir = file;
}
// Save as APKS first
File tempFile = FileCache.getGlobalFileCache().createCachedFile("apks");
try {
Path tempPath = Paths.get(tempFile);
SplitApkExporter.saveApks(packageInfo, tempPath);
String path = packageInfo.packageName + ApkUtils.EXT_APKS;
SplitApkExporter.addFile(zipOutputStream, tempPath, path, exportTimestamp);
// Add as dependency
Dependency dependency = new Dependency();
dependency.packageName = packageInfo.packageName;
dependency.displayName = packageInfo.applicationInfo.loadLabel(pm).toString();
dependency.versionName = packageInfo.versionName;
dependency.versionCode = PackageInfoCompat.getLongVersionCode(packageInfo);
dependency.required = true;
dependency.signatures = null;
dependency.match = Dependency.DEPENDENCY_MATCH_EXACT;
dependency.path = path;
dependencies.add(dependency);
} finally {
FileCache.getGlobalFileCache().delete(tempFile);
}
}
}
// Write meta
byte[] meta = getMetadataAsJson().getBytes(StandardCharsets.UTF_8);
SplitApkExporter.addBytes(zipOutputStream, meta, ApksMetadata.META_FILE, exportTimestamp);
}
@NonNull
public String getMetadataAsJson() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("info_version", metaVersion);
jsonObject.put("package_name", packageName);
jsonObject.put("display_name", displayName);
jsonObject.put("version_name", versionName);
jsonObject.put("version_code", versionCode);
jsonObject.put("min_sdk", minSdk);
jsonObject.put("target_sdk", targetSdk);
// Skip build info for privacy
// Put dependencies
JSONArray dependenciesArray = new JSONArray();
for (Dependency dependency : dependencies) {
JSONObject dependencyObject = new JSONObject();
dependencyObject.put("package_name", dependency.packageName);
dependencyObject.put("display_name", dependency.displayName);
dependencyObject.put("version_name", dependency.versionName);
dependencyObject.put("version_code", dependency.versionCode);
if (dependency.signatures != null) {
dependencyObject.put("signature", TextUtils.join(",", dependency.signatures));
}
dependencyObject.put("match", dependency.match);
dependencyObject.put("required", dependency.required);
if (dependency.path != null) {
dependencyObject.put("path", dependency.path);
}
dependenciesArray.put(dependencyObject);
}
if (dependenciesArray.length() > 0) {
jsonObject.put("dependencies", dependenciesArray);
}
} catch (JSONException e) {
e.printStackTrace();
}
return jsonObject.toString();
}
}
================================================
FILE: app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkChooser.java
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
package io.github.muntashirakon.AppManager.apk.splitapk;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import aosp.libcore.util.EmptyArray;
import io.github.muntashirakon.AppManager.apk.ApkFile;
import io.github.muntashirakon.AppManager.apk.ApkSource;
import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerViewModel;
import io.github.muntashirakon.AppManager.utils.ArrayUtils;
import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder;
public class SplitApkChooser extends Fragment {
public static final String TAG = SplitApkChooser.class.getSimpleName();
private static final String EXTRA_ACTION_NAME = "name";
private static final String EXTRA_VERSION_INFO = "version";
@NonNull
public static SplitApkChooser getNewInstance(@NonNull String versionInfo, @Nullable String actionName) {
SplitApkChooser splitApkChooser = new SplitApkChooser();
Bundle args = new Bundle();
args.putString(EXTRA_ACTION_NAME, actionName);
args.putString(EXTRA_VERSION_INFO, versionInfo);
splitApkChooser.setArguments(args);
return splitApkChooser;
}
private PackageInstallerViewModel mViewModel;
private List mApkEntries;
private SearchableMultiChoiceDialogBuilder mViewBuilder;
private Set mSelectedSplits;
private final HashMap /* seen types */> mSeenSplits = new HashMap<>();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewModel = new ViewModelProvider(requireActivity()).get(PackageInstallerViewModel.class);
mSelectedSplits = mViewModel.getSelectedSplits();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ApkFile apkFile = mViewModel.getApkFile();
if (apkFile == null) {
throw new IllegalArgumentException("ApkFile cannot be empty.");
}
if (!apkFile.isSplit()) {
throw new RuntimeException("Apk file does not contain any split.");
}
mApkEntries = apkFile.getEntries();
String[] entryIds = new String[mApkEntries.size()];
CharSequence[] entryNames = new CharSequence[mApkEntries.size()];
for (int i = 0; i < mApkEntries.size(); ++i) {
ApkFile.Entry entry = mApkEntries.get(i);
entryIds[i] = entry.id;
entryNames[i] = entry.toLocalizedString(requireActivity());
}
mViewBuilder = new SearchableMultiChoiceDialogBuilder<>(requireActivity(), entryIds, entryNames)
.showSelectAll(false)
.addDisabledItems(getUnsupportedOrRequiredSplitIds());
mViewBuilder.create(); // Necessary to trigger multichoice dialog
return mViewBuilder.getView();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
mViewBuilder.addSelections(getInitialSelections())
.setOnMultiChoiceClickListener((dialog, which, item, isChecked) -> {
if (isChecked) {
mViewBuilder.addSelections(select(which));
} else {
int[] itemsToDeselect = deselect(which);
if (itemsToDeselect == null) {
// This item can't be deselected, reselect the item
mViewBuilder.addSelections(new int[]{which});
} else {
mViewBuilder.removeSelections(itemsToDeselect);
}
}
mViewBuilder.reloadListUi();
});
}
@NonNull
public int[] getInitialSelections() {
List selections = new ArrayList<>();
try {
HashSet splitNames = new HashSet<>();
// See if the app has been installed
if (mViewModel.getInstalledPackageInfo() != null) {
ApplicationInfo info = mViewModel.getInstalledPackageInfo().applicationInfo;
try (ApkFile installedApkFile = ApkSource.getApkSource(info).resolve()) {
for (ApkFile.Entry apkEntry : installedApkFile.getEntries()) {
splitNames.add(apkEntry.name);
}
}
}
if (splitNames.size() > 0) {
for (ApkFile.Entry apkEntry : mApkEntries) {
if (!splitNames.contains(apkEntry.name)) {
// Ignore splits that weren't selected in the previous installation
continue;
}
mSelectedSplits.add(apkEntry.id);
HashSet types = mSeenSplits.get(apkEntry.getFeature());
if (types == null) {
types = new HashSet<>();
mSeenSplits.put(apkEntry.getFeature(), types);
}
types.add(apkEntry.type);
}
// Fall-through deliberately to see if there are any new requirements
}
} catch (ApkFile.ApkFileException ignored) {
}
// Set up features
for (int i = 0; i < mApkEntries.size(); ++i) {
ApkFile.Entry apkEntry = mApkEntries.get(i);
if (mSelectedSplits.contains(apkEntry.id)) {
// Features already set
selections.add(i);
continue;
}
if (apkEntry.isRequired()) {
// Required splits are selected by default
mSelectedSplits.add(apkEntry.id);
selections.add(i);
HashSet types = mSeenSplits.get(apkEntry.getFeature());
if (types == null) {
types = new HashSet<>();
mSeenSplits.put(apkEntry.getFeature(), types);
}
types.add(apkEntry.type);
}
}
// Select feature-dependencies based on the items selected above.
// Only selecting the first item works because the splits are already ranked.
for (int i = 0; i < mApkEntries.size(); ++i) {
ApkFile.Entry apkEntry = mApkEntries.get(i);
if (mSelectedSplits.contains(apkEntry.id)) {
// Already selected
continue;
}
HashSet types = mSeenSplits.get(apkEntry.getFeature());
if (types == null) {
// This feature was not selected earlier
continue;
}
switch (apkEntry.type) {
case ApkFile.APK_BASE:
case ApkFile.APK_SPLIT_FEATURE:
case ApkFile.APK_SPLIT_UNKNOWN:
case ApkFile.APK_SPLIT:
// Never reached.
break;
case ApkFile.APK_SPLIT_DENSITY:
if (!types.contains(ApkFile.APK_SPLIT_DENSITY)) {
types.add(ApkFile.APK_SPLIT_DENSITY);
selections.add(i);
mSelectedSplits.add(apkEntry.id);
}
break;
case ApkFile.APK_SPLIT_ABI:
if (!types.contains(ApkFile.APK_SPLIT_ABI)) {
types.add(ApkFile.APK_SPLIT_ABI);
selections.add(i);
mSelectedSplits.add(apkEntry.id);
}
break;
case ApkFile.APK_SPLIT_LOCALE:
if (!types.contains(ApkFile.APK_SPLIT_LOCALE)) {
types.add(ApkFile.APK_SPLIT_LOCALE);
selections.add(i);
mSelectedSplits.add(apkEntry.id);
}
break;
default:
throw new RuntimeException("Invalid split type.");
}
}
return ArrayUtils.convertToIntArray(selections);
}
@NonNull
private List getUnsupportedOrRequiredSplitIds() {
List