diff --git a/.github/workflows/platform_emulator_release.yml b/.github/workflows/platform_emulator_release.yml
new file mode 100644
index 00000000000..bae43fa5495
--- /dev/null
+++ b/.github/workflows/platform_emulator_release.yml
@@ -0,0 +1,50 @@
+name: Platform Emulator Release
+
+on:
+ push:
+ branches:
+ - 'master'
+ - 'AppManager-*'
+ paths-ignore:
+ - 'fastlane/**'
+ - 'scripts/**'
+ - '*.md'
+ pull_request:
+ branches: [ master ]
+ paths-ignore:
+ - 'fastlane/**'
+ - 'scripts/**'
+ - '*.md'
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the repository
+ uses: actions/checkout@v4
+ with:
+ submodules: 'recursive'
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'zulu'
+ java-version: '21'
+ cache: 'gradle'
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Get app version
+ run: echo "APP_VERSION=v$(cat ./app/build.gradle | grep -m1 versionName | awk -F \" '{print $2}')" >> $GITHUB_ENV
+ - name: Get app name
+ run: echo "APP_NAME=AppManager_${{ env.APP_VERSION }}-PLATFORM#${{ github.run_number }}" >> $GITHUB_ENV
+ - name: Inject run number
+ run: sed -i -e 's|versionName "\([0-9\.]\+\)"|versionName "\1-${{ github.run_number }}"|' ./app/build.gradle
+ - name: Build with Gradle
+ run: ./gradlew packagePlatformReleaseUniversalApk -Pandroid.injected.signing.store.file=$PWD/app/dev_keystore.jks -Pandroid.injected.signing.store.password='kJCp!Bda#PBdN2RLK%yMK@hatq&69E' -Pandroid.injected.signing.key.alias=key1 -Pandroid.injected.signing.key.password='kJCp!Bda#PBdN2RLK%yMK@hatq&69E'
+ - name: Rename the APK file
+ run: mv ./app/build/outputs/apk_from_bundle/platformRelease/app-platform-release-universal.apk ./${{ env.APP_NAME }}.apk
+ - name: Store generated APK file
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.APP_NAME }}
+ path: ./${{ env.APP_NAME }}.apk
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 00000000000..7bd35e3c73c
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,36 @@
+android_app_import {
+ name: "AppManager",
+ owner: "Muntashirakon",
+ apk: "app/build/outputs/apk_from_bundle/platformRelease/app-platform-release-universal-unsigned.apk",
+ certificate: "platform",
+ dex_preopt: {
+ enabled: false,
+ },
+ privileged: true,
+ presigned: false,
+ overrides: ["PackageInstaller"],
+ required: [
+ "io.github.muntashirakon.AppManager_sysconfig.xml",
+ "default-permissions_io.github.muntashirakon.AppManager.xml",
+ "privapp-permissions_io.github.muntashirakon.AppManager.xml"
+ ],
+}
+
+
+prebuilt_etc {
+ name: "io.github.muntashirakon.AppManager_sysconfig.xml",
+ src: "platform/sysconfig.xml",
+ sub_dir: "sysconfig",
+}
+
+prebuilt_etc {
+ name: "privapp-permissions_io.github.muntashirakon.AppManager.xml",
+ src: "platform/permissions.xml",
+ sub_dir: "permissions",
+}
+
+prebuilt_etc {
+ name: "default-permissions_io.github.muntashirakon.AppManager.xml",
+ src: "platform/default-permissions.xml",
+ sub_dir: "default-permissions"
+}
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
index c2fdceef700..27cc6bff3c3 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -2,3 +2,4 @@
*.apk
.cxx
*~
+/platform
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index c341aed556e..411ed3be518 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -40,21 +40,40 @@ android {
keyPassword 'kJCp!Bda#PBdN2RLK%yMK@hatq&69E'
keyAlias 'key0'
}
+ platform { // Platform signing key used by eng rom images https://github.com/LineageOS/android_build/blob/lineage-23.2/target/product/security/platform.pk8
+ storeFile file('dev_keystore.jks')
+ storePassword 'kJCp!Bda#PBdN2RLK%yMK@hatq&69E'
+ keyPassword 'kJCp!Bda#PBdN2RLK%yMK@hatq&69E'
+ keyAlias 'key1'
+ }
}
-
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "string", "app_name", "App Manager"
}
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
- signingConfig signingConfigs.debug
resValue "string", "app_name", "AM Debug"
+ // Value must be changed manually for platform debug
+ // TODO 11FEB26 make debug signingConfig selection automatic.
+ signingConfig = signingConfigs.platform;
}
}
+ flavorDimensions "version"
+ productFlavors {
+ standard {
+ dimension "version"
+ }
+ platform {
+ dimension "version"
+ applicationIdSuffix ".platform"
+ versionNameSuffix "-PLATFORM"
+ }
+ }
+
lint {
checkReleaseBuilds false
abortOnError false
diff --git a/app/dev_keystore.jks b/app/dev_keystore.jks
index c2abe7bb892..9466e5438f2 100644
Binary files a/app/dev_keystore.jks and b/app/dev_keystore.jks differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c2e07c4609d..e9c05983fc4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -152,7 +152,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -728,7 +746,12 @@
android:label="@string/interceptor"
android:taskAffinity=""
android:windowSoftInputMode="stateUnchanged">
-
+
+
+
+
+
+
diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/AppManager.java b/app/src/main/java/io/github/muntashirakon/AppManager/AppManager.java
index 79a3f945971..85923903509 100644
--- a/app/src/main/java/io/github/muntashirakon/AppManager/AppManager.java
+++ b/app/src/main/java/io/github/muntashirakon/AppManager/AppManager.java
@@ -15,6 +15,7 @@
import org.lsposed.hiddenapibypass.HiddenApiBypass;
import java.security.Security;
+import java.util.Objects;
import dalvik.system.ZipPathValidator;
import io.github.muntashirakon.AppManager.misc.AMExceptionHandler;
@@ -48,7 +49,8 @@ public void onCreate() {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !Utils.isRoboUnitTest()) {
+ //noinspection ConstantValue Don't use the HiddenApiBypass in Platform as we are whilelisted and it caused infighting if enabled
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !Utils.isRoboUnitTest() && !Objects.equals(BuildConfig.FLAVOR, "platform")) {
HiddenApiBypass.addHiddenApiExemptions("L");
}
}
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..7bf4431a0f2 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
@@ -146,6 +146,26 @@ 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) {
+ mTitleBuilder.setTitle(R.string.confirm_installation)
+ .setStartIcon(R.drawable.ic_get_app)
+ .setSubtitle(null)
+ .setEndIcon(null, null);
+ // 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 50c17dabefa..48cd9025404 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,17 +25,21 @@
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageInstallerSession;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstallerHidden;
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;
import android.text.style.RelativeSizeSpan;
import android.view.View;
+import android.view.WindowHidden;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -45,10 +51,12 @@
import androidx.core.content.pm.PackageInfoCompat;
import androidx.lifecycle.ViewModelProvider;
+import java.io.File;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Queue;
+import dev.rikka.tools.refine.Refine;
import io.github.muntashirakon.AppManager.BaseActivity;
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
@@ -109,11 +117,14 @@ public static Intent getLaunchableInstance(@NonNull Context context, ApkSource a
@NonNull
public static Intent getLaunchableInstance(@NonNull Context context, @NonNull String packageName) {
Intent intent = new Intent(context, PackageInstallerActivity.class);
- intent.setData(Uri.parse("package:" + packageName));
+ intent.putExtra(EXTRA_INSTALL_EXISTING, true);
+ intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
return intent;
}
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 ACTION_PACKAGE_INSTALLED = BuildConfig.APPLICATION_ID + ".action.PACKAGE_INSTALLED";
private int mSessionId = -1;
@@ -194,6 +205,11 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
onNewIntent(intent);
return;
}
+ // Take a page out of the official installer hide all other overlays that aren't the system
+ if (SelfPermissions.checkSelfPermission("android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS")) {
+ Refine.unsafeCast(getWindow()).addSystemFlags(1 << 19);
+ }
+
mModel = new ViewModelProvider(this).get(PackageInstallerViewModel.class);
if (!bindService(
new Intent(this, PackageInstallerService.class), mServiceConnection, BIND_AUTO_CREATE)) {
@@ -203,7 +219,18 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
mApkQueue.addAll(ApkQueueItem.fromIntent(intent, Utils.getRealReferrer(this)));
}
- ApkSource apkSource = IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_APK_FILE_LINK, ApkSource.class);
+ ApkSource apkSource;
+ if (ACTION_CONFIRM_INSTALL.equals(intent.getAction()) || ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())) {
+ mSessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
+ try {
+ Uri uri = Uri.fromFile(new File(Refine.unsafeCast(PackageManagerCompat.getPackageInstaller().getSessionInfo(mSessionId)).getResolvedBaseApkPath()));
+ apkSource = ApkSource.getApkSource(uri, null);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ apkSource = IntentCompat.getUnwrappedParcelableExtra(intent, EXTRA_APK_FILE_LINK, ApkSource.class);
+ }
if (apkSource != null) {
synchronized (mApkQueue) {
mApkQueue.add(ApkQueueItem.fromApkSource(apkSource));
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 c536eadcf36..88f98cd1bdf 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
@@ -89,6 +89,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,
@@ -813,6 +816,15 @@ 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);
+ }
+ }
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean openSession(@UserIdInt int userId, @InstallFlags int installFlags,
diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/self/life/BuildExpiryChecker.java b/app/src/main/java/io/github/muntashirakon/AppManager/self/life/BuildExpiryChecker.java
index 671a9b8d8f4..86d2f9481ec 100644
--- a/app/src/main/java/io/github/muntashirakon/AppManager/self/life/BuildExpiryChecker.java
+++ b/app/src/main/java/io/github/muntashirakon/AppManager/self/life/BuildExpiryChecker.java
@@ -20,9 +20,10 @@
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
+import io.github.muntashirakon.AppManager.settings.Ops;
public final class BuildExpiryChecker {
- @IntDef({BUILD_TYPE_DEBUG, BUILD_TYPE_ALPHA, BUILD_TYPE_BETA, BUILD_TYPE_RC, BUILD_TYPE_STABLE})
+ @IntDef({BUILD_TYPE_DEBUG, BUILD_TYPE_ALPHA, BUILD_TYPE_BETA, BUILD_TYPE_RC, BUILD_TYPE_STABLE, BUILD_TYPE_PLATFORM})
@Retention(RetentionPolicy.SOURCE)
private @interface BuildType {}
@@ -31,6 +32,7 @@ public final class BuildExpiryChecker {
private static final int BUILD_TYPE_BETA = 2;
private static final int BUILD_TYPE_RC = 3;
private static final int BUILD_TYPE_STABLE = 4;
+ private static final int BUILD_TYPE_PLATFORM = 5;
private static final long[] TIME_SPAN_MILLIS = new long[]{
2L * 30 * 24 * 60 * 60_000, // 2 months
@@ -38,6 +40,7 @@ public final class BuildExpiryChecker {
6L * 30 * 24 * 60 * 60_000, // 6 months
6L * 30 * 24 * 60 * 60_000, // 6 months
18L * 30 * 24 * 60 * 60_000, // 18 months
+ 18L * 30 * 24 * 60 * 60_000, // 18 months
};
private static final long[] WARNING_PERIOD_MILLIS = new long[]{
@@ -46,6 +49,7 @@ public final class BuildExpiryChecker {
30L * 24 * 60 * 60_000, // 1 month
30L * 24 * 60 * 60_000, // 1 month
3L * 30 * 24 * 60 * 60_000, // 3 months
+ 3L * 30 * 24 * 60 * 60_000, // 3 months
};
@NonNull
@@ -69,7 +73,8 @@ public static AlertDialog getBuildExpiredDialog(@NonNull FragmentActivity activi
activity.startActivity(intent);
activity.finishAndRemoveTask();
});
- if (getBuildType() == BUILD_TYPE_STABLE) {
+ // Platform
+ if (getBuildType() == BUILD_TYPE_STABLE || getBuildType() == BUILD_TYPE_PLATFORM) {
builder.setNeutralButton(R.string.action_continue, continueClickListener);
}
return builder.create();
@@ -107,6 +112,9 @@ private static Uri getUpdateUri() {
if (getBuildType() == BUILD_TYPE_DEBUG) {
return Uri.parse("https://github.com/MuntashirAkon/AppManager/actions/workflows/debug_build.yml");
}
+ if (getBuildType() == BUILD_TYPE_PLATFORM) { // Rom Builders Should Set this value //TODO: 8FEB26 Link to relevant AM Docs page
+ return Uri.parse(System.getProperty("ro.appmanager.update.url", "https://tharow.net/AppManager"));
+ }
// TODO: 3/12/22 For Stable builds, check F-Droid too
return Uri.parse("https://github.com/MuntashirAkon/AppManager/releases");
}
@@ -116,6 +124,9 @@ private static int getBuildType() {
if (BuildConfig.DEBUG) {
return BUILD_TYPE_DEBUG;
}
+ if (Ops.isPlatform()) {
+ return BUILD_TYPE_PLATFORM;
+ }
String[] versionParts = BuildConfig.VERSION_NAME.split("-");
if (versionParts.length == 1) {
return BUILD_TYPE_STABLE;
diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferences.java b/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferences.java
index 8793357e9ba..f3e4ec14c3a 100644
--- a/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferences.java
+++ b/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferences.java
@@ -33,13 +33,6 @@ public static MainPreferences getInstance(@Nullable String key, boolean dualPane
return preferences;
}
- private static final List MODE_NAMES = Arrays.asList(
- Ops.MODE_AUTO,
- Ops.MODE_ROOT,
- Ops.MODE_ADB_OVER_TCP,
- Ops.MODE_ADB_WIFI,
- Ops.MODE_NO_ROOT);
-
private FragmentActivity mActivity;
private Preference mModePref;
private Preference mLocalePref;
@@ -76,7 +69,7 @@ public void onStart() {
super.onStart();
if (mModePref != null) {
mModePref.setSummary(getString(R.string.mode_of_op_with_inferred_mode_of_op,
- mModes[MODE_NAMES.indexOf(Ops.getMode())], Ops.getInferredMode(mActivity)));
+ mModes[Ops.MODE_NAMES.indexOf(Ops.getMode())], Ops.getInferredMode(mActivity)));
}
if (mLocalePref != null) {
mLocalePref.setSummary(getLanguageName());
diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/settings/ModeOfOpsPreference.java b/app/src/main/java/io/github/muntashirakon/AppManager/settings/ModeOfOpsPreference.java
index 2b2fbefde23..d24b4212fe9 100644
--- a/app/src/main/java/io/github/muntashirakon/AppManager/settings/ModeOfOpsPreference.java
+++ b/app/src/main/java/io/github/muntashirakon/AppManager/settings/ModeOfOpsPreference.java
@@ -41,13 +41,6 @@
import io.github.muntashirakon.widget.TextInputTextView;
public class ModeOfOpsPreference extends Fragment {
- private static final List MODE_NAMES = Arrays.asList(
- Ops.MODE_AUTO,
- Ops.MODE_ROOT,
- Ops.MODE_ADB_OVER_TCP,
- Ops.MODE_ADB_WIFI,
- Ops.MODE_NO_ROOT);
-
private MaterialTextView mInferredModeView;
private MaterialTextView mRemoteServerStatusView;
private MaterialTextView mRemoteServicesStatusView;
@@ -113,7 +106,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Utils.isTv(requireContext())) {
disabledItems = Collections.singletonList(Ops.MODE_ADB_WIFI);
} else disabledItems = null;
- changeModeView.setOnClickListener(v -> new SearchableSingleChoiceDialogBuilder<>(requireActivity(), MODE_NAMES, mModes)
+ changeModeView.setOnClickListener(v -> new SearchableSingleChoiceDialogBuilder<>(requireActivity(), Ops.MODE_NAMES, mModes)
.setTitle(R.string.pref_mode_of_operations)
.setSelection(mCurrentMode)
.addDisabledItems(disabledItems)
@@ -210,7 +203,7 @@ private void updateViews() {
TextViewCompat.setCompoundDrawableTintList(mModeOfOpsView, mColorActive);
mModeOfOpsView.setTextColor(mColorActive);
mModeOfOpsView.setCompoundDrawablesRelativeWithIntrinsicBounds(mIconProgress, 0, 0, 0);
- mModeOfOpsView.setText(getString(R.string.status_connecting_via_mode, mModes[MODE_NAMES.indexOf(mCurrentMode)]));
+ mModeOfOpsView.setText(getString(R.string.status_connecting_via_mode, mModes[Ops.MODE_NAMES.indexOf(mCurrentMode)]));
} else {
int uid = Users.getSelfOrRemoteUid();
boolean goodMode = !badInferredMode(mCurrentMode, uid);
@@ -223,14 +216,14 @@ private void updateViews() {
CharSequence mode;
if (serverActive && uid != Process.myUid()) {
mode = "remote service";
- } else mode = mModes[MODE_NAMES.indexOf(mCurrentMode)];
+ } else mode = mModes[Ops.MODE_NAMES.indexOf(mCurrentMode)];
mModeOfOpsView.setText(getString(R.string.status_connected_via_mode, mode));
} else {
mInferredModeView.setTextColor(mColorError);
TextViewCompat.setCompoundDrawableTintList(mModeOfOpsView, mColorError);
mModeOfOpsView.setTextColor(mColorError);
mModeOfOpsView.setCompoundDrawablesRelativeWithIntrinsicBounds(mIconInactive, 0, 0, 0);
- mModeOfOpsView.setText(getString(R.string.status_not_connected_via_mode, mModes[MODE_NAMES.indexOf(mCurrentMode)]));
+ mModeOfOpsView.setText(getString(R.string.status_not_connected_via_mode, mModes[Ops.MODE_NAMES.indexOf(mCurrentMode)]));
}
}
// Server
diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/settings/Ops.java b/app/src/main/java/io/github/muntashirakon/AppManager/settings/Ops.java
index 86e5e48944a..5943b58e37b 100644
--- a/app/src/main/java/io/github/muntashirakon/AppManager/settings/Ops.java
+++ b/app/src/main/java/io/github/muntashirakon/AppManager/settings/Ops.java
@@ -29,11 +29,14 @@
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
+import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.adb.AdbConnectionManager;
import io.github.muntashirakon.AppManager.adb.AdbPairingService;
@@ -66,7 +69,7 @@
public class Ops {
public static final String TAG = Ops.class.getSimpleName();
- @StringDef({MODE_AUTO, MODE_ROOT, MODE_ADB_OVER_TCP, MODE_ADB_WIFI, MODE_NO_ROOT})
+ @StringDef({MODE_AUTO, MODE_ROOT, MODE_ADB_OVER_TCP, MODE_ADB_WIFI, MODE_NO_ROOT, MODE_PLATFORM})
@Retention(RetentionPolicy.SOURCE)
public @interface Mode {
}
@@ -76,6 +79,15 @@ public class Ops {
public static final String MODE_ADB_OVER_TCP = "adb_tcp";
public static final String MODE_ADB_WIFI = "adb_wifi";
public static final String MODE_NO_ROOT = "no-root";
+ public static final String MODE_PLATFORM = "platform";
+
+ public static final List MODE_NAMES = Arrays.asList(
+ Ops.MODE_AUTO,
+ Ops.MODE_ROOT,
+ Ops.MODE_ADB_OVER_TCP,
+ Ops.MODE_ADB_WIFI,
+ Ops.MODE_NO_ROOT,
+ Ops.MODE_PLATFORM);
@IntDef({
STATUS_SUCCESS,
@@ -166,6 +178,12 @@ public static boolean isAdb() {
return sIsAdb;
}
+ @AnyThread
+ public static boolean isPlatform() {
+ //noinspection ConstantValue
+ return "platform".equals(BuildConfig.FLAVOR);
+ }
+
/**
* Whether the current App Manager session is authenticated by the user. It does two things:
*
@@ -260,6 +278,9 @@ public static int init(@NonNull Context context, boolean force) {
}
return STATUS_SUCCESS;
}
+ if (MODE_PLATFORM.equals(mode)) {
+ return initPermissionsWithSuccess();
+ }
if (!force && isAMServiceUpAndRunning(context, mode)) {
// An instance of AMService is already running
return sIsAdb ? STATUS_SUCCESS : initPermissionsWithSuccess();
@@ -325,6 +346,9 @@ public static boolean hasRoot() {
@NoOps // Although we've used Ops checks, its overall usage does not affect anything
private static void autoDetectRootSystemOrAdbAndPersist(@NonNull Context context) {
sIsRoot = sDirectRoot;
+ if (isPlatform()) {
+ setMode(MODE_PLATFORM);
+ }
if (sDirectRoot) {
// Root permission was granted
setMode(MODE_ROOT);
diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/utils/Utils.java b/app/src/main/java/io/github/muntashirakon/AppManager/utils/Utils.java
index 06a9120675e..6e9d16d0fa6 100644
--- a/app/src/main/java/io/github/muntashirakon/AppManager/utils/Utils.java
+++ b/app/src/main/java/io/github/muntashirakon/AppManager/utils/Utils.java
@@ -53,6 +53,7 @@
import io.github.muntashirakon.AppManager.apk.signing.SignerInfo;
import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
import io.github.muntashirakon.AppManager.misc.OsEnvironment;
+import io.github.muntashirakon.AppManager.self.SelfPermissions;
public class Utils {
public static final String TERMUX_LOGIN_PATH = OsEnvironment.getDataDirectoryRaw() + "/data/com.termux/files/usr/bin/login";
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 4b6287b254a..b3f944a2640 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -36,6 +36,7 @@
- @string/adb_over_tcp
- @string/wireless_debugging
- @string/no_root
+ - @string/platform
- @string/type_boolean
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 83b4d6306e3..33f48f66ea2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1558,4 +1558,5 @@
Selected %d out of %d items
ADB local server port
Waiting for Wi-Fi
+ Platform
diff --git a/app/src/platform/AndroidManifest.xml b/app/src/platform/AndroidManifest.xml
new file mode 100644
index 00000000000..89c3180b69b
--- /dev/null
+++ b/app/src/platform/AndroidManifest.xml
@@ -0,0 +1,334 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/platform/java/io/github/muntashirakon/AppManager/apk/installer/PackageUninstallerActivity.java b/app/src/platform/java/io/github/muntashirakon/AppManager/apk/installer/PackageUninstallerActivity.java
new file mode 100644
index 00000000000..ce40b7e70e4
--- /dev/null
+++ b/app/src/platform/java/io/github/muntashirakon/AppManager/apk/installer/PackageUninstallerActivity.java
@@ -0,0 +1,98 @@
+package io.github.muntashirakon.AppManager.apk.installer;
+
+import static io.github.muntashirakon.AppManager.utils.UIUtils.displayLongToast;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandleHidden;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+
+import java.util.Objects;
+
+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;
+
+ @Override
+ public boolean getTransparentBackground() {
+ return true;
+ }
+
+
+ @RequiresPermission(anyOf = {Manifest.permission.REQUEST_DELETE_PACKAGES, Manifest.permission.DELETE_PACKAGES})
+ @Override
+ protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
+ this.mPackageName = Objects.requireNonNull(getIntent().getData()).getHost();
+ boolean 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 | PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Uninstaller: onAuthenticated: getApplicationInfo", e);
+ throw new RuntimeException(e);
+ }
+ this.mAppLabel = mApplicationInfo.loadLabel(getPackageManager()).toString();
+ uninstallDialog(isUninstallFromAllUsers);
+ }
+
+ private void uninstallDialog(boolean isUninstallFromAllUsers) {
+ 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)
+ .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();
+ finish();
+ });
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/hiddenapi/src/main/java/android/content/pm/PackageInstallerHidden.java b/hiddenapi/src/main/java/android/content/pm/PackageInstallerHidden.java
index 84ef335cfe9..272e390939c 100644
--- a/hiddenapi/src/main/java/android/content/pm/PackageInstallerHidden.java
+++ b/hiddenapi/src/main/java/android/content/pm/PackageInstallerHidden.java
@@ -54,4 +54,10 @@ public static class SessionParams {
@RequiresApi(Build.VERSION_CODES.P)
public String installerPackageName;
}
+
+ public static class SessionInfo {
+ public String getResolvedBaseApkPath() {
+ return HiddenUtil.throwUOE();
+ }
+ }
}
diff --git a/hiddenapi/src/main/java/android/view/WindowHidden.java b/hiddenapi/src/main/java/android/view/WindowHidden.java
new file mode 100644
index 00000000000..04e6e45cb87
--- /dev/null
+++ b/hiddenapi/src/main/java/android/view/WindowHidden.java
@@ -0,0 +1,11 @@
+package android.view;
+
+import dev.rikka.tools.refine.RefineAs;
+import misc.utils.HiddenUtil;
+
+@RefineAs(Window.class)
+public class WindowHidden {
+ public void addSystemFlags(int flags) {
+ HiddenUtil.throwUOE(flags);
+ }
+}
diff --git a/platform/default-permissions.xml b/platform/default-permissions.xml
new file mode 100644
index 00000000000..d27fd8823df
--- /dev/null
+++ b/platform/default-permissions.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/permissions.xml b/platform/permissions.xml
new file mode 100644
index 00000000000..fe3e23233bb
--- /dev/null
+++ b/platform/permissions.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/sysconfig.xml b/platform/sysconfig.xml
new file mode 100644
index 00000000000..8645eb15a8c
--- /dev/null
+++ b/platform/sysconfig.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+