diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c9634c8341e..90182bfc3ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -147,7 +147,9 @@ tools:ignore="ProtectedPermissions" /> - + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerDialogHelper.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerDialogHelper.java index 7797732eb81..50565b07166 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerDialogHelper.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/InstallerDialogHelper.java @@ -3,6 +3,7 @@ package io.github.muntashirakon.AppManager.apk.installer; import android.content.Context; +import android.content.pm.PackageInstaller; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.Button; @@ -146,6 +147,22 @@ public void showInstallConfirmationDialog(@StringRes int installButtonRes, mMessage.setText(R.string.install_app_message); mFragmentContainer.setVisibility(View.GONE); } + public void showSessionConfirmationDialog(@StringRes int installButtonRes, + @NonNull OnClickButtonsListener onClickButtonsListener) { + // Buttons + mNeutralBtn.setVisibility(View.GONE); + mPositiveBtn.setVisibility(View.VISIBLE); + mPositiveBtn.setText(installButtonRes); + mPositiveBtn.setOnClickListener(v -> onClickButtonsListener.triggerInstall()); + mNegativeBtn.setVisibility(View.VISIBLE); + mNegativeBtn.setText(R.string.cancel); + mNegativeBtn.setOnClickListener(v -> onClickButtonsListener.triggerCancel()); + // Body + mLayout.setVisibility(View.GONE); + mMessage.setVisibility(View.VISIBLE); + mMessage.setText(R.string.install_app_message); + mFragmentContainer.setVisibility(View.GONE); + } public void showApkChooserDialog(@StringRes int installButtonRes, Fragment fragment, @NonNull OnClickButtonsListener onClickButtonsListener, diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java index 91638af4907..93b0ec9ac3a 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java @@ -2,6 +2,8 @@ package io.github.muntashirakon.AppManager.apk.installer; +import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.ACTION_CONFIRM_INSTALL; +import static io.github.muntashirakon.AppManager.apk.installer.PackageInstallerCompat.ACTION_CONFIRM_PRE_APPROVAL; 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; @@ -23,12 +25,14 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageInstaller; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandleHidden; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -43,6 +47,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.pm.PackageInfoCompat; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import java.util.LinkedList; @@ -116,7 +121,7 @@ public static Intent getLaunchableInstance(@NonNull Context context, @NonNull St private static final String EXTRA_APK_FILE_LINK = "link"; public static final String EXTRA_INSTALL_EXISTING = "install_existing"; - public static final String EXTRA_PACKAGE_NAME = "pkg"; + public static final String EXTRA_PACKAGE_NAME = PackageInstaller.EXTRA_PACKAGE_NAME; public static final String ACTION_PACKAGE_INSTALLED = BuildConfig.APPLICATION_ID + ".action.PACKAGE_INSTALLED"; private int mSessionId = -1; @@ -197,6 +202,11 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) { onNewIntent(intent); return; } + if (ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()) || + ACTION_CONFIRM_INSTALL.equals(intent.getAction())) { + onSessionIntent(intent); + return; + } mModel = new ViewModelProvider(this).get(PackageInstallerViewModel.class); if (!bindService( new Intent(this, PackageInstallerService.class), mServiceConnection, BIND_AUTO_CREATE)) { @@ -376,12 +386,55 @@ private void launchInstallerService() { goToNext(); } } + private void onSessionIntent(@NonNull Intent intent) { + mSessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); + IPackageInstaller installer; + PackageInstaller.SessionInfo sessionInfo; + PackageInstallerCompat installerCompat = PackageInstallerCompat.getNewInstance(); + try { + installer = PackageManagerCompat.getPackageInstaller(); + sessionInfo = installer.getSessionInfo(mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to getPackageInstaller", e); + throw new RuntimeException(e); + } + + InstallerDialogHelper.OnClickButtonsListener onClickButtonsListener = new InstallerDialogHelper.OnClickButtonsListener() { + @Override + public void triggerInstall() { + installerCompat.setPermissionsResult(mSessionId, true); + } + + @Override + public void triggerCancel() { + installerCompat.setPermissionsResult(mSessionId, false); + } + }; + + 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"); + } + + // Init fragment + mInstallerDialogFragment = new InstallerDialogFragment(); + mInstallerDialogFragment.setCancelable(false); + mInstallerDialogFragment.onCreateDialog(null); + mInstallerDialogFragment.showNow(getSupportFragmentManager(), InstallerDialogFragment.TAG); + if (ACTION_CONFIRM_INSTALL.equals(intent.getAction())) { + mDialogHelper.showSessionConfirmationDialog(R.string.install, onClickButtonsListener); + } else if (ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())) { + mDialogHelper.showSessionConfirmationDialog(R.string.install, onClickButtonsListener); + } + } @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); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java index 13c62649167..111b1be5ff0 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java @@ -35,10 +35,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; import androidx.annotation.WorkerThread; import androidx.core.app.PendingIntentCompat; import androidx.core.content.ContextCompat; +import org.jetbrains.annotations.Contract; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -84,6 +87,9 @@ public final class PackageInstallerCompat { // 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"; + public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL"; + public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL"; + public static final String EXTRA_UNINSTALL_ALL_USERS = "android.intent.extra.UNINSTALL_ALL_USERS"; @IntDef({ STATUS_SUCCESS, @@ -797,6 +803,30 @@ private boolean commit(int userId) { } return mFinalStatus == PackageInstaller.STATUS_SUCCESS; } + public void setPermissionsResult(int sessionId, boolean accepted) { + try { + mPackageInstaller = PackageManagerCompat.getPackageInstaller(); + mPackageInstaller.setPermissionsResult(sessionId, accepted); + } catch (RemoteException e) { + callFinish(STATUS_FAILURE_SESSION_COMMIT); + Log.e(TAG, "SetPermissionsResult: failed to set permission result for given session", e); + } + + } + + @RequiresPermission("android.permission.PACKAGE_VERIFICATION_AGENT") + public IPackageInstallerSession openSession(final int sessionId) { + + try { + mPackageInstaller = PackageManagerCompat.getPackageInstaller(); + return Refine.unsafeCast(new PackageInstallerHidden.Session(IPackageInstallerSession.Stub.asInterface( + new ProxyBinder(mPackageInstaller.openSession(sessionId).asBinder())))); + } catch (RemoteException e) { + callFinish(STATUS_FAILURE_SESSION_CREATE); + Log.e(TAG, "OpenSession: Failed to open install session.", e); + return null; + } + } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean openSession(@UserIdInt int userId, @InstallFlags int installFlags, diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageUninstallerActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageUninstallerActivity.java new file mode 100644 index 00000000000..fb4b01637c5 --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageUninstallerActivity.java @@ -0,0 +1,115 @@ +package io.github.muntashirakon.AppManager.apk.installer; + +import static io.github.muntashirakon.AppManager.utils.UIUtils.displayLongToast; + +import android.Manifest; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandleHidden; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; + +import io.github.muntashirakon.AppManager.BaseActivity; +import io.github.muntashirakon.AppManager.R; +import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; +import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; +import io.github.muntashirakon.AppManager.self.SelfPermissions; +import io.github.muntashirakon.AppManager.utils.ThreadUtils; +import io.github.muntashirakon.AppManager.utils.UIUtils; +import io.github.muntashirakon.dialog.ScrollableDialogBuilder; + +public class PackageUninstallerActivity extends BaseActivity { + public static final String TAG = PackageUninstallerActivity.class.getSimpleName(); + private String mPackageName; + private ApplicationInfo mApplicationInfo; + private BaseActivity mActivity; + private String mAppLabel; + private int mUserId; + private boolean isUninstallFromAllUsers; + + @Override + public boolean getTransparentBackground() { + return true; + } + + + @RequiresPermission(android.Manifest.permission.REQUEST_DELETE_PACKAGES) + @Override + protected void onAuthenticated(@Nullable Bundle savedInstanceState) { + this.mPackageName = getIntent().getDataString().replaceFirst("package:", ""); + this.isUninstallFromAllUsers = getIntent().getBooleanExtra(PackageInstallerCompat.EXTRA_UNINSTALL_ALL_USERS, false); + this.mActivity = this; + this.mUserId = UserHandleHidden.myUserId(); + try { + this.mApplicationInfo = PackageManagerCompat.getApplicationInfo(mPackageName, ApplicationInfo.FLAG_INSTALLED, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + this.mAppLabel = mApplicationInfo.loadLabel(getPackageManager()).toString(); + uninstallDialog(); + } + + private void uninstallDialog() { + if (mUserId != UserHandleHidden.myUserId() && !SelfPermissions.checkSelfOrRemotePermission(Manifest.permission.DELETE_PACKAGES)) { + // Could be for work profile + try { + Intent uninstallIntent = new Intent(Intent.ACTION_DELETE); + uninstallIntent.setData(Uri.parse("package:" + mPackageName)); + ActivityManagerCompat.startActivity(uninstallIntent, mUserId); + // TODO: 19/8/24 Watch for uninstallation + } catch (Throwable th) { + UIUtils.displayLongToast("Error: " + th.getLocalizedMessage()); + } + return; + } + final boolean isSystemApp = (mApplicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + ScrollableDialogBuilder builder = new ScrollableDialogBuilder(mActivity, + isSystemApp ? R.string.uninstall_system_app_message : R.string.uninstall_app_message) + .setTitle(mAppLabel) + // FIXME: 16/6/23 Does it even work without INSTALL_PACKAGES? + .setCheckboxLabel(R.string.keep_data_and_app_signing_signatures) + .setPositiveButton(R.string.uninstall, (dialog, which, keepData) -> ThreadUtils.postOnBackgroundThread(() -> { + PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); + installer.setAppLabel(mAppLabel); + boolean uninstalled = installer.uninstall(mPackageName, mUserId, keepData); + ThreadUtils.postOnMainThread(() -> { + if (uninstalled) { + displayLongToast(R.string.uninstalled_successfully, mAppLabel); + mActivity.finish(); + } else { + displayLongToast(R.string.failed_to_uninstall, mAppLabel); + } + }); + })) + .setNegativeButton(R.string.cancel, (dialog, which, keepData) -> { + if (dialog != null) dialog.cancel(); + }); + if ((mApplicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + builder.setNeutralButton(R.string.uninstall_updates, (dialog, which, keepData) -> + ThreadUtils.postOnBackgroundThread(() -> { + PackageInstallerCompat installer = PackageInstallerCompat.getNewInstance(); + installer.setAppLabel(mAppLabel); + boolean isSuccessful = installer.uninstall(mPackageName, UserHandleHidden.USER_ALL, keepData); + if (isSuccessful) { + ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.update_uninstalled_successfully, mAppLabel)); + } else { + ThreadUtils.postOnMainThread(() -> displayLongToast(R.string.failed_to_uninstall_updates, mAppLabel)); + } + })); + } + builder.show(); + } +}