From 4f97928e97fd9da5a93c7ea1af40c682462b4e33 Mon Sep 17 00:00:00 2001 From: Muntashir Al-Islam Date: Mon, 22 Apr 2024 18:40:59 +0600 Subject: [PATCH] [Settings] Add a fallback server runner command from DE storage Added a fallback server runner command in case SD card is inaccessible from the UID from which the script is being run. This leverages the device encrypted storage (data_de) by making certain folder and files accessibly globally. Even this workaround may not work in some devices, especially Android 12+ ROM where a better protection has been introduced. Signed-off-by: Muntashir Al-Islam --- .../AppManager/servermanager/AssetsUtils.java | 61 ++++--------- .../AppManager/servermanager/LocalServer.java | 17 ---- .../servermanager/LocalServerManager.java | 14 +-- .../servermanager/ServerConfig.java | 90 ++++++++++++------- .../settings/MainPreferencesViewModel.java | 20 +++-- .../settings/ModeOfOpsPreference.java | 21 +++-- .../AppManager/utils/FileUtils.java | 10 +++ .../main/res/layout/fragment_mode_of_ops.xml | 26 ++++++ app/src/main/res/values/strings.xml | 3 +- 9 files changed, 148 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/AssetsUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/AssetsUtils.java index a1238cb705a..9ffa961045a 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/AssetsUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/AssetsUtils.java @@ -4,12 +4,12 @@ import android.content.Context; import android.content.res.AssetFileDescriptor; -import android.text.TextUtils; -import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; +import org.jetbrains.annotations.NotNull; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -18,14 +18,10 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; -import java.security.SecureRandom; import io.github.muntashirakon.AppManager.BuildConfig; -import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.server.common.ConfigParams; import io.github.muntashirakon.AppManager.server.common.Constants; -import io.github.muntashirakon.AppManager.utils.ContextUtils; -import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.io.IoUtils; // Copyright 2016 Zheng Li @@ -57,66 +53,47 @@ public static void copyFile(@NonNull Context context, String fileName, File dest fos.flush(); fos.getFD().sync(); } - FileUtils.chmod644(destFile); } } @WorkerThread - static void writeScript(@NonNull Context context) throws IOException { - try (AssetFileDescriptor openFd = context.getAssets().openFd(ServerConfig.EXECUTABLE_FILE_NAME); - FileInputStream fdInputStream = openFd.createInputStream(); - InputStreamReader inputStreamReader = new InputStreamReader(fdInputStream); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { - File destFile = ServerConfig.getExecPath(); + static void writeServerExecScript(@NonNull Context context, @NonNull File destFile, @NonNull String classPath) throws IOException { + try (AssetFileDescriptor openFd = context.getAssets().openFd(ServerConfig.SERVER_RUNNER_EXEC_NAME); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(openFd.createInputStream()))) { if (destFile.exists()) { destFile.delete(); } - StringBuilder sb = new StringBuilder(); - sb.append(',').append(ConfigParams.PARAM_APP).append(':').append(BuildConfig.APPLICATION_ID); - - if (ServerConfig.getAllowBgRunning()) { - sb.append(',').append(ConfigParams.PARAM_RUN_IN_BACKGROUND).append(':').append(1); - } - if (BuildConfig.DEBUG) { - sb.append(',').append(ConfigParams.PARAM_DEBUG).append(':').append(1); - } - - String classpath = ServerConfig.getClassPath(); - String args = sb.toString(); - try (BufferedWriter bw = new BufferedWriter(new FileWriter(destFile, false))) { // Set variables StringBuilder script = new StringBuilder(); script.append("SERVER_NAME=").append(Constants.SERVER_NAME).append("\n") .append("JAR_NAME=").append(Constants.JAR_NAME).append("\n") - .append("JAR_PATH=").append(classpath).append("\n") - .append("ARGS=").append(args).append("\n"); - String line = bufferedReader.readLine(); - while (line != null) { + .append("JAR_PATH=").append(classPath).append("\n") + .append("ARGS=").append(getServerArgs()).append("\n"); + String line; + while ((line = bufferedReader.readLine()) != null) { String wl; if ("%ENV_VARS%".equals(line.trim())) { wl = script.toString(); } else wl = line; bw.write(wl); bw.newLine(); - line = bufferedReader.readLine(); } bw.flush(); } - FileUtils.chmod644(destFile); } } - @AnyThread - @NonNull - static String generateToken() { - Context context = ContextUtils.getContext(); - String[] wordList = context.getResources().getStringArray(R.array.word_list); - SecureRandom secureRandom = new SecureRandom(); - String[] tokenItems = new String[3 + secureRandom.nextInt(3)]; - for (int i = 0; i < tokenItems.length; ++i) { - tokenItems[i] = wordList[secureRandom.nextInt(wordList.length)]; + @NotNull + private static String getServerArgs() { + StringBuilder argsBuilder = new StringBuilder(); + argsBuilder.append(',').append(ConfigParams.PARAM_APP).append(':').append(BuildConfig.APPLICATION_ID); + if (ServerConfig.getAllowBgRunning()) { + argsBuilder.append(',').append(ConfigParams.PARAM_RUN_IN_BACKGROUND).append(':').append(1); + } + if (BuildConfig.DEBUG) { + argsBuilder.append(',').append(ConfigParams.PARAM_DEBUG).append(':').append(1); } - return TextUtils.join("-", tokenItems); + return argsBuilder.toString(); } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServer.java b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServer.java index 87c15d97ef4..7f5c9afd9c7 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServer.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServer.java @@ -17,7 +17,6 @@ import java.net.ServerSocket; import java.net.SocketTimeoutException; -import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; import io.github.muntashirakon.AppManager.server.common.Caller; @@ -89,8 +88,6 @@ private LocalServer() throws IOException, AdbPairingRequiredException { mLocalServerManager = LocalServerManager.getInstance(mContext); // Initialise necessary files and permissions ServerConfig.init(mContext, UserHandleHidden.myUserId()); - // Check if am.jar is in the right place - checkFile(); // Start server if not already checkConnect(); } @@ -168,13 +165,6 @@ public void closeBgServer() { mLocalServerManager.stop(); } - @WorkerThread - @NoOps - private void checkFile() throws IOException { - AssetsUtils.copyFile(mContext, ServerConfig.JAR_NAME, ServerConfig.getDestJarFile(), BuildConfig.DEBUG); - AssetsUtils.writeScript(mContext); - } - @WorkerThread @NoOps(used = true) public static void restart() throws IOException, AdbPairingRequiredException { @@ -187,11 +177,4 @@ public static void restart() throws IOException, AdbPairingRequiredException { getInstance(); } } - - @WorkerThread - @NonNull - public static String getExecCommand(@NonNull Context context) throws IOException { - AssetsUtils.writeScript(context); - return "sh " + ServerConfig.getExecPath() + " " + ServerConfig.getLocalServerPort() + " " + ServerConfig.getLocalToken(); - } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServerManager.java b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServerManager.java index aeb4e447407..b149ab8cf8a 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServerManager.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/LocalServerManager.java @@ -172,14 +172,6 @@ void closeBgServer() { } } - @WorkerThread - @NonNull - private String getExecCommand() throws IOException { - Log.e(TAG, "classpath --> %s", ServerConfig.getClassPath()); - Log.e(TAG, "exec path --> %s", ServerConfig.getExecPath()); - return LocalServer.getExecCommand(mContext); - } - @Nullable private volatile AdbStream mAdbStream; private volatile CountDownLatch mAdbConnectionWatcher = new CountDownLatch(1); @@ -227,7 +219,8 @@ private void useAdbStartServer() throws Exception { try (OutputStream os = Objects.requireNonNull(mAdbStream).openOutputStream()) { os.write("id\n".getBytes()); - String command = getExecCommand(); + // ADB may require a fallback method + String command = ServerConfig.getServerRunnerAdbCommand(); Log.d(TAG, "useAdbStartServer: %s", command); os.write((command + "\n").getBytes()); } @@ -243,7 +236,8 @@ private void useRootStartServer() throws Exception { if (!Ops.hasRoot()) { throw new Exception("Root access denied"); } - String command = getExecCommand(); // + "\n" + "supolicy --live 'allow qti_init_shell zygote_exec file execute'"; + String command = ServerConfig.getServerRunnerCommand(0); + // + "\n" + "supolicy --live 'allow qti_init_shell zygote_exec file execute'"; Log.d(TAG, "useRootStartServer: %s", command); Runner.Result result = Runner.runCommand(command); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/ServerConfig.java b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/ServerConfig.java index 723eafe8459..47a7fb34660 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/ServerConfig.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/servermanager/ServerConfig.java @@ -4,7 +4,6 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; -import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; @@ -19,25 +18,29 @@ import java.io.File; import java.io.IOException; import java.net.Inet4Address; +import java.security.SecureRandom; +import io.github.muntashirakon.AppManager.BuildConfig; +import io.github.muntashirakon.AppManager.R; +import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.misc.NoOps; +import io.github.muntashirakon.AppManager.server.common.Constants; import io.github.muntashirakon.AppManager.utils.ContextUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; // Copyright 2016 Zheng Li public final class ServerConfig { + public static final String TAG = ServerConfig.class.getSimpleName(); + public static final int DEFAULT_ADB_PORT = 5555; - static String SOCKET_PATH = "am_socket"; - private static int DEFAULT_LOCAL_SERVER_PORT = 60001; + static final String SERVER_RUNNER_EXEC_NAME = "run_server.sh"; + private static final int DEFAULT_LOCAL_SERVER_PORT = 60001; private static final String LOCAL_TOKEN = "l_token"; - - static final String JAR_NAME = "am.jar"; - static final String EXECUTABLE_FILE_NAME = "run_server.sh"; - - private static File sDestJarFile; - private static File sDestExecFile; + private static final File[] SERVER_RUNNER_EXEC = new File[2]; + private static final File[] SERVER_RUNNER_JAR = new File[2]; private static final SharedPreferences sPreferences = ContextUtils.getContext() .getSharedPreferences("server_config", Context.MODE_PRIVATE); + private static int sServerPort = DEFAULT_LOCAL_SERVER_PORT; private static volatile boolean sInitialised = false; @WorkerThread @@ -47,13 +50,27 @@ public static void init(@NonNull Context context, @UserIdInt int userHandle) thr return; } - File externalStorage = FileUtils.getExternalCachePath(context); - sDestJarFile = new File(externalStorage, JAR_NAME); - sDestExecFile = new File(externalStorage, EXECUTABLE_FILE_NAME); - + // Setup paths + File externalCachePath = FileUtils.getExternalCachePath(context); + File externalMediaPath = FileUtils.getExternalMediaPath(context); + File deStorage = ContextUtils.getDeContext(context).getCacheDir(); + SERVER_RUNNER_EXEC[0] = new File(externalCachePath, SERVER_RUNNER_EXEC_NAME); + SERVER_RUNNER_EXEC[1] = new File(deStorage, SERVER_RUNNER_EXEC_NAME); + SERVER_RUNNER_JAR[0] = new File(externalCachePath, Constants.JAR_NAME); + SERVER_RUNNER_JAR[1] = new File(deStorage, Constants.JAR_NAME); + // Copy JAR + boolean force = BuildConfig.DEBUG; + AssetsUtils.copyFile(context, Constants.JAR_NAME, SERVER_RUNNER_JAR[0], force); + AssetsUtils.copyFile(context, Constants.JAR_NAME, SERVER_RUNNER_JAR[1], force); + // Write script + AssetsUtils.writeServerExecScript(context, SERVER_RUNNER_EXEC[0], SERVER_RUNNER_JAR[0].getAbsolutePath()); + AssetsUtils.writeServerExecScript(context, SERVER_RUNNER_EXEC[1], SERVER_RUNNER_JAR[1].getAbsolutePath()); + // Update permission + FileUtils.chmod711(deStorage); + FileUtils.chmod644(SERVER_RUNNER_JAR[1]); + FileUtils.chmod644(SERVER_RUNNER_EXEC[1]); if (userHandle != 0) { - SOCKET_PATH += userHandle; - DEFAULT_LOCAL_SERVER_PORT += userHandle; + sServerPort += userHandle; } sInitialised = true; @@ -62,19 +79,22 @@ public static void init(@NonNull Context context, @UserIdInt int userHandle) thr @AnyThread @NonNull public static File getDestJarFile() { - return sDestJarFile; + // For compatibility only + return SERVER_RUNNER_JAR[0]; } @AnyThread @NonNull - static String getClassPath() { - return sDestJarFile.getAbsolutePath(); + public static String getServerRunnerCommand(int index) throws IndexOutOfBoundsException { + Log.e(TAG, "Classpath: %s", SERVER_RUNNER_JAR[index]); + Log.e(TAG, "Exec path: %s", SERVER_RUNNER_EXEC[index]); + return "sh " + SERVER_RUNNER_EXEC[index] + " " + getLocalServerPort() + " " + getLocalToken(); } @AnyThread @NonNull - public static File getExecPath() { - return sDestExecFile; + public static String getServerRunnerAdbCommand() throws IndexOutOfBoundsException { + return getServerRunnerCommand(0) + " || " + getServerRunnerCommand(1); } /** @@ -84,17 +104,17 @@ public static File getExecPath() { */ @AnyThread @NonNull - static String getLocalToken() { + public static String getLocalToken() { String token = sPreferences.getString(LOCAL_TOKEN, null); if (TextUtils.isEmpty(token)) { - token = AssetsUtils.generateToken(); + token = generateToken(); sPreferences.edit().putString(LOCAL_TOKEN, token).apply(); } return token; } @AnyThread - static boolean getAllowBgRunning() { + public static boolean getAllowBgRunning() { return sPreferences.getBoolean("allow_bg_running", true); } @@ -112,8 +132,8 @@ public static void setAdbPort(@IntRange(from = 0, to = 65535) int port) { } @AnyThread - static int getLocalServerPort() { - return DEFAULT_LOCAL_SERVER_PORT; + public static int getLocalServerPort() { + return sServerPort; } @WorkerThread @@ -125,12 +145,6 @@ public static String getAdbHost(Context context) { @WorkerThread @NonNull public static String getLocalServerHost(Context context) { - return getHostIpAddress(); - } - - @WorkerThread - @NonNull - private static String getHostIpAddress() { String ipAddress = Inet4Address.getLoopbackAddress().getHostAddress(); if (ipAddress == null || ipAddress.equals("::1")) return "127.0.0.1"; return ipAddress; @@ -158,4 +172,18 @@ private static boolean isEmulator(@NonNull Context context) { || Build.HARDWARE.contains(RANCHU) || androidId == null; } + + + @AnyThread + @NonNull + private static String generateToken() { + Context context = ContextUtils.getContext(); + String[] wordList = context.getResources().getStringArray(R.array.word_list); + SecureRandom secureRandom = new SecureRandom(); + String[] tokenItems = new String[3 + secureRandom.nextInt(3)]; + for (int i = 0; i < tokenItems.length; ++i) { + tokenItems[i] = wordList[secureRandom.nextInt(wordList.length)]; + } + return TextUtils.join("-", tokenItems); + } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferencesViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferencesViewModel.java index 5ba31ea9632..a8a4a511281 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferencesViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/settings/MainPreferencesViewModel.java @@ -39,7 +39,6 @@ import io.github.muntashirakon.AppManager.misc.DeviceInfo2; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; -import io.github.muntashirakon.AppManager.servermanager.LocalServer; import io.github.muntashirakon.AppManager.servermanager.ServerConfig; import io.github.muntashirakon.AppManager.users.UserInfo; import io.github.muntashirakon.AppManager.users.Users; @@ -54,7 +53,8 @@ public class MainPreferencesViewModel extends AndroidViewModel implements Ops.Ad private final MutableLiveData> mSelectUsers = new SingleLiveEvent<>(); private final MutableLiveData mChangeLog = new SingleLiveEvent<>(); private final MutableLiveData mDeviceInfo = new SingleLiveEvent<>(); - private final MutableLiveData mCustomCommand = new SingleLiveEvent<>(); + private final MutableLiveData mCustomCommand0 = new SingleLiveEvent<>(); + private final MutableLiveData mCustomCommand1 = new SingleLiveEvent<>(); private final MutableLiveData mModeOfOpsStatus = new SingleLiveEvent<>(); private final MutableLiveData mOperationCompletedLiveData = new SingleLiveEvent<>(); private final MutableLiveData> mStorageVolumesLiveData = new SingleLiveEvent<>(); @@ -115,18 +115,24 @@ public void reloadApps() { }); } - public MutableLiveData getCustomCommand() { - return mCustomCommand; + public MutableLiveData getCustomCommand0() { + return mCustomCommand0; } - public void loadCustomCommand() { + public MutableLiveData getCustomCommand1() { + return mCustomCommand1; + } + + public void loadCustomCommands() { mExecutor.submit(() -> { try { ServerConfig.init(getApplication(), UserHandleHidden.myUserId()); - mCustomCommand.postValue(LocalServer.getExecCommand(getApplication())); + mCustomCommand0.postValue(ServerConfig.getServerRunnerCommand(0)); + mCustomCommand1.postValue(ServerConfig.getServerRunnerCommand(1)); } catch (Throwable e) { e.printStackTrace(); - mCustomCommand.postValue(null); + mCustomCommand0.postValue(null); + mCustomCommand1.postValue(null); } }); } 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 0f69ed61b5f..98d13d42402 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 @@ -121,15 +121,23 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat }) .setNegativeButton(R.string.cancel, null) .show()); - TextInputTextView customCommand = view.findViewById(android.R.id.text1); - TextInputLayout customCommandLayout = TextInputLayoutCompat.fromTextInputEditText(customCommand); - customCommandLayout.setEndIconOnClickListener(v -> { - CharSequence command = customCommand.getText(); + TextInputTextView customCommand0 = view.findViewById(android.R.id.text1); + TextInputLayout customCommand0Layout = TextInputLayoutCompat.fromTextInputEditText(customCommand0); + customCommand0Layout.setEndIconOnClickListener(v -> { + CharSequence command = customCommand0.getText(); if (!TextUtils.isEmpty(command)) { Utils.copyToClipboard(requireContext(), "command", command); } }); - mModel.loadCustomCommand(); + TextInputTextView customCommand1 = view.findViewById(android.R.id.text2); + TextInputLayout customCommand1Layout = TextInputLayoutCompat.fromTextInputEditText(customCommand1); + customCommand1Layout.setEndIconOnClickListener(v -> { + CharSequence command = customCommand1.getText(); + if (!TextUtils.isEmpty(command)) { + Utils.copyToClipboard(requireContext(), "command", command); + } + }); + mModel.loadCustomCommands(); updateViews(); // Mode of ops mModel.getModeOfOpsStatus().observe(getViewLifecycleOwner(), status -> { @@ -169,7 +177,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat updateViews(); } }); - mModel.getCustomCommand().observe(getViewLifecycleOwner(), customCommand::setText); + mModel.getCustomCommand0().observe(getViewLifecycleOwner(), customCommand0::setText); + mModel.getCustomCommand1().observe(getViewLifecycleOwner(), customCommand1::setText); } @Override diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java index 82412504dfa..79fc6d003bd 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java @@ -207,6 +207,16 @@ public static File getBestExternalPath(@Nullable File[] extDirs) throws IOExcept throw new FileNotFoundException(lastReason != null ? lastReason : "No available shared storage found."); } + @AnyThread + public static void chmod711(@NonNull File file) throws IOException { + try { + Os.chmod(file.getAbsolutePath(), 457); + } catch (ErrnoException e) { + Log.e("IOUtils", "Failed to apply mode 711 to " + file); + throw new IOException(e); + } + } + @AnyThread public static void chmod644(@NonNull File file) throws IOException { try { diff --git a/app/src/main/res/layout/fragment_mode_of_ops.xml b/app/src/main/res/layout/fragment_mode_of_ops.xml index 00880859f30..3c65152206e 100644 --- a/app/src/main/res/layout/fragment_mode_of_ops.xml +++ b/app/src/main/res/layout/fragment_mode_of_ops.xml @@ -106,6 +106,7 @@ style="@style/Widget.AppTheme.TextInputLayout.Small" android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingVertical="@dimen/padding_small" app:endIconContentDescription="@string/copy" app:endIconMode="custom" app:endIconDrawable="@drawable/ic_content_copy"> @@ -121,6 +122,31 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86a682e3381..d81fed91edb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1484,5 +1484,6 @@ Pairing… Retry Custom Command - If you are unable to use any of the modes, you can run the following command using any supported shell to run App Manager in privileged mode: + If you are unable to use any of the modes, you can run the following command in any supported shell to run App Manager in privileged mode: + If you get a “permission denied” error with the above command, run the following command instead: