├── .github └── workflows │ └── debug_build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── net │ │ └── dinglisch │ │ └── android │ │ └── appfactory │ │ └── apktools │ │ └── commandrunner │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── bootstrapZips │ │ ├── bootstrap-aarch64.zip │ │ ├── bootstrap-arm.zip │ │ ├── bootstrap-i686.zip │ │ └── bootstrap-x86_64.zip │ ├── java │ │ └── net │ │ │ └── dinglisch │ │ │ └── android │ │ │ └── appfactory │ │ │ ├── AppFactoryApplication.java │ │ │ ├── AppFactoryConstants.java │ │ │ ├── MainActivity.java │ │ │ └── utils │ │ │ ├── ApkTools.java │ │ │ ├── BootstrapInstaller.java │ │ │ ├── android │ │ │ └── PackageUtils.java │ │ │ ├── data │ │ │ └── DataUtils.java │ │ │ ├── errors │ │ │ ├── Errno.java │ │ │ ├── Error.java │ │ │ └── FunctionErrno.java │ │ │ ├── file │ │ │ ├── FileUtils.java │ │ │ ├── FileUtilsErrno.java │ │ │ ├── filesystem │ │ │ │ ├── FileAttributes.java │ │ │ │ ├── FileKey.java │ │ │ │ ├── FilePermission.java │ │ │ │ ├── FilePermissions.java │ │ │ │ ├── FileTime.java │ │ │ │ ├── FileType.java │ │ │ │ ├── FileTypes.java │ │ │ │ ├── NativeDispatcher.java │ │ │ │ └── UnixConstants.java │ │ │ └── tests │ │ │ │ └── FileUtilsTests.java │ │ │ ├── logger │ │ │ └── Logger.java │ │ │ ├── markdown │ │ │ └── MarkdownUtils.java │ │ │ └── shell │ │ │ ├── ExecutionCommand.java │ │ │ ├── ShellUtils.java │ │ │ ├── StreamGobbler.java │ │ │ ├── environment │ │ │ ├── AndroidShellEnvironment.java │ │ │ ├── AppShellEnvironment.java │ │ │ ├── IShellEnvironment.java │ │ │ ├── ShellEnvironmentUtils.java │ │ │ ├── ShellEnvironmentVariable.java │ │ │ └── UnixShellEnvironment.java │ │ │ ├── result │ │ │ └── ResultData.java │ │ │ └── runner │ │ │ └── app │ │ │ └── AppShell.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── background.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── net │ └── dinglisch │ └── android │ └── appfactory │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/debug_build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v2 17 | - name: Build 18 | run: | 19 | ./gradlew assembleDebug 20 | - name: Store generated APK file 21 | uses: actions/upload-artifact@v2 22 | with: 23 | name: termux-app 24 | path: ./app/build/outputs/apk/debug 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | 10 | /app/src/main/bootstrapZips/*.done 11 | /app/src/main/bootstrapLibs/ 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The `TaskerAppFactory` repo is released under [MIT](https://opensource.org/licenses/MIT) license. 2 | 3 | ### Exceptions 4 | 5 | #### [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html) 6 | 7 | - [`app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/*`](app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem) files that use code from [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/). 8 | ## 9 | 10 | 11 | #### [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) 12 | 13 | - [`app/src/main/java/net/dinglisch/android/appfactory/utils/shell/StreamGobbler.java`](app/src/main/java/net/dinglisch/android/appfactory/utils/shell/StreamGobbler.java) uses code from [libsuperuser ](https://github.com/Chainfire/libsuperuser). 14 | ## 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### TaskerAppFactory 2 | 3 | POC for fixing Tasker App Factory issue of not being able to build apks that have `targetSdkVersion >= 30` as per https://www.reddit.com/r/tasker/comments/vz1s0f/comment/igwmgg9/ to decompress `resources.arsc` and `zipalign` the `APK` with `zip` , `unzip` and `zipalign` binaries. 4 | 5 | Bootstrap zips are compiled for `net.dinglisch.android.appfactory` package as per https://github.com/termux/termux-packages/wiki/For-maintainers#build-bootstrap-archives with the packages `aapt`, `zip` and `unzip` only and only support android `>= 7`, with additional files removed manually. The bootstrap zips cannot be used in other app packages or will get linker errors due to wrong `$PREFIX` of libraries. 6 | ## 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | defaultConfig { 6 | applicationId "net.dinglisch.android.appfactory" 7 | minSdkVersion 21 8 | //noinspection OldTargetApi 9 | targetSdkVersion 30 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | 14 | ndk { 15 | abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' 16 | } 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility 1.8 27 | targetCompatibility 1.8 28 | } 29 | 30 | packagingOptions { 31 | jniLibs { 32 | useLegacyPackaging true 33 | } 34 | } 35 | 36 | sourceSets { 37 | main { 38 | jniLibs.srcDirs = ["src/main/bootstrapLibs"] 39 | } 40 | } 41 | } 42 | 43 | dependencies { 44 | testImplementation 'junit:junit:4.13.2' 45 | androidTestImplementation 'androidx.test:runner:1.4.0' 46 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 47 | 48 | implementation fileTree(dir: 'libs', include: ['*.jar']) 49 | 50 | implementation "androidx.annotation:annotation:1.3.0" 51 | implementation "androidx.appcompat:appcompat:1.3.1" 52 | implementation "androidx.core:core:1.6.0" 53 | implementation "com.google.guava:guava:24.1-jre" 54 | 55 | // Required for FileUtils 56 | // Do not increment version higher than 2.5 or there 57 | // will be runtime exceptions on android < 8 58 | // due to missing classes like java.nio.file.Path. 59 | implementation "commons-io:commons-io:2.5" 60 | } 61 | 62 | def expandBootstrap(File bootstrapZip, String expectedChecksum, String arch) { 63 | def doneMarkerFile = new File(bootstrapZip.getAbsolutePath() + "." + expectedChecksum + ".done") 64 | if (doneMarkerFile.exists()) return 65 | 66 | def archDirName 67 | if (arch == "aarch64") archDirName = "arm64-v8a"; 68 | if (arch == "arm") archDirName = "armeabi-v7a"; 69 | if (arch == "i686") archDirName = "x86"; 70 | if (arch == "x86_64") archDirName = "x86_64"; 71 | 72 | def outputPath = project.getRootDir().getAbsolutePath() + "/app/src/main/bootstrapLibs/" + archDirName + "/" 73 | def outputDir = new File(outputPath).getAbsoluteFile() 74 | if (!outputDir.exists()) outputDir.mkdirs() 75 | 76 | def symlinksFile = new File(outputDir, "libsymlinks.so").getAbsoluteFile() 77 | if (symlinksFile.exists()) symlinksFile.delete(); 78 | 79 | def mappingsFile = new File(outputDir, "libfiles.so").getAbsoluteFile() 80 | if (mappingsFile.exists()) mappingsFile.delete() 81 | mappingsFile.createNewFile() 82 | def mappingsFileWriter = new BufferedWriter(new FileWriter(mappingsFile)) 83 | 84 | def counter = 100 85 | new java.util.zip.ZipInputStream(new FileInputStream(bootstrapZip)).withCloseable { zipInput -> 86 | def zipEntry 87 | while ((zipEntry = zipInput.getNextEntry()) != null) { 88 | if (zipEntry.getName() == "SYMLINKS.txt") { 89 | zipInput.transferTo(new FileOutputStream(symlinksFile)) 90 | } else if (!zipEntry.isDirectory()) { 91 | def soName = "lib" + counter + ".so" 92 | def targetFile = new File(outputDir, soName).getAbsoluteFile() 93 | 94 | println "target file path is ${targetFile}" 95 | 96 | try { 97 | zipInput.transferTo(new FileOutputStream(targetFile)) 98 | } catch (Exception e) { 99 | println "Error ${e}" 100 | } 101 | 102 | mappingsFileWriter.writeLine(soName + "←" + zipEntry.getName()) 103 | counter++ 104 | } 105 | } 106 | } 107 | 108 | mappingsFileWriter.close() 109 | doneMarkerFile.createNewFile() 110 | } 111 | 112 | def setupBootstraps(String arch) { 113 | def digest = java.security.MessageDigest.getInstance("SHA-256") 114 | 115 | def file = new File(projectDir, "src/main/bootstrapZips/bootstrap-" + arch + ".zip"); 116 | if (file.exists()) { 117 | def buffer = new byte[8192] 118 | def input = new FileInputStream(file) 119 | while (true) { 120 | def readBytes = input.read(buffer) 121 | if (readBytes < 0) break 122 | digest.update(buffer, 0, readBytes) 123 | } 124 | def checksum = new BigInteger(1, digest.digest()).toString(16) 125 | expandBootstrap(file, checksum, arch) 126 | } else { 127 | throw new GradleException("Failed to find " + arch + " bootstrap at \"" + file.getAbsolutePath() + "\"") 128 | } 129 | } 130 | 131 | clean { 132 | doLast { 133 | def tree = fileTree(new File(projectDir, 'src/main')) 134 | //tree.include 'bootstrapZips/bootstrap-*.zip' 135 | tree.include 'bootstrapZips/bootstrap-*.zip.*.done' 136 | tree.include 'bootstrapLibs/*/*' 137 | tree.each { it.delete() } 138 | } 139 | } 140 | 141 | task setupBootstraps() { 142 | doLast { 143 | setupBootstraps("aarch64") 144 | setupBootstraps("arm") 145 | setupBootstraps("i686") 146 | setupBootstraps("x86_64") 147 | } 148 | } 149 | 150 | afterEvaluate { 151 | android.applicationVariants.all { variant -> 152 | variant.javaCompileProvider.get().dependsOn(setupBootstraps) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/net/dinglisch/android/appfactory/apktools/commandrunner/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("net.dinglisch.android.appfactory", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 10 | 11 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/bootstrapZips/bootstrap-aarch64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agnostic-apollo/TaskerAppFactory/dbf319ffdb39e99ceb43cdcddc0799a3698be840/app/src/main/bootstrapZips/bootstrap-aarch64.zip -------------------------------------------------------------------------------- /app/src/main/bootstrapZips/bootstrap-arm.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agnostic-apollo/TaskerAppFactory/dbf319ffdb39e99ceb43cdcddc0799a3698be840/app/src/main/bootstrapZips/bootstrap-arm.zip -------------------------------------------------------------------------------- /app/src/main/bootstrapZips/bootstrap-i686.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agnostic-apollo/TaskerAppFactory/dbf319ffdb39e99ceb43cdcddc0799a3698be840/app/src/main/bootstrapZips/bootstrap-i686.zip -------------------------------------------------------------------------------- /app/src/main/bootstrapZips/bootstrap-x86_64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agnostic-apollo/TaskerAppFactory/dbf319ffdb39e99ceb43cdcddc0799a3698be840/app/src/main/bootstrapZips/bootstrap-x86_64.zip -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/AppFactoryApplication.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import net.dinglisch.android.appfactory.utils.logger.Logger; 7 | 8 | public class AppFactoryApplication extends Application { 9 | 10 | private static final String LOG_TAG = "TermuxApplication"; 11 | 12 | public void onCreate() { 13 | super.onCreate(); 14 | 15 | Context context = getApplicationContext(); 16 | 17 | // Set log config for the app 18 | setLogConfig(context); 19 | 20 | Logger.logDebug("Starting Application"); 21 | 22 | AppFactoryConstants.init(this); 23 | } 24 | 25 | public static void setLogConfig(Context context) { 26 | Logger.setDefaultLogTag(AppFactoryConstants.APP_NAME); 27 | 28 | Logger.setLogLevel(null, Logger.LOG_LEVEL_VERBOSE); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/AppFactoryConstants.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | public class AppFactoryConstants { 8 | 9 | /** App name */ 10 | public static final String APP_NAME = "AppFactory"; // Default: "AppFactory" 11 | /** App package name */ 12 | public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; // Default: "net.dinglisch.android.appfactory" 13 | 14 | 15 | 16 | /* 17 | * Termux app core directory paths. 18 | */ 19 | 20 | 21 | /** Termux app Files directory path */ 22 | public static String FILES_DIR_PATH; // Default: "/data/data/net.dinglisch.android.appfactory/files" 23 | 24 | /** Termux app $PREFIX directory path */ 25 | public static String PREFIX_DIR_PATH; // Default: "/data/data/net.dinglisch.android.appfactory/files/usr" 26 | 27 | /** Termux app $PREFIX/bin directory path */ 28 | public static String BIN_PREFIX_DIR_PATH; // Default: "/data/data/net.dinglisch.android.appfactory/files/usr/bin" 29 | 30 | /** Termux app $PREFIX/lib directory path */ 31 | public static String LIB_PREFIX_DIR_PATH; // Default: "/data/data/net.dinglisch.android.appfactory/files/usr/lib" 32 | 33 | /** Termux app $PREFIX/tmp and $TMPDIR directory path */ 34 | public static String TMP_PREFIX_DIR_PATH; // Default: "/data/data/net.dinglisch.android.appfactory/files/usr/tmp" 35 | 36 | /** Termux app $HOME directory path */ 37 | public static String HOME_DIR_PATH; // Default: "/data/data/net.dinglisch.android.appfactory/files/home" 38 | 39 | 40 | 41 | public static void init(@NonNull Context context) { 42 | FILES_DIR_PATH = context.getFilesDir().getAbsolutePath().replace("/data/user/0/", "/data/data/"); 43 | 44 | PREFIX_DIR_PATH = FILES_DIR_PATH + "/usr"; 45 | BIN_PREFIX_DIR_PATH = PREFIX_DIR_PATH + "/bin"; 46 | LIB_PREFIX_DIR_PATH = PREFIX_DIR_PATH + "/lib"; 47 | TMP_PREFIX_DIR_PATH = PREFIX_DIR_PATH + "/tmp"; 48 | 49 | HOME_DIR_PATH = FILES_DIR_PATH + "/home"; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/MainActivity.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | import android.widget.Button; 7 | import android.widget.EditText; 8 | import android.widget.TextView; 9 | 10 | import net.dinglisch.android.appfactory.utils.ApkTools; 11 | import net.dinglisch.android.appfactory.utils.BootstrapInstaller; 12 | import net.dinglisch.android.appfactory.utils.logger.Logger; 13 | 14 | public class MainActivity extends AppCompatActivity { 15 | 16 | private static final String LOG_TAG = "MainActivity"; 17 | 18 | private volatile boolean executing = false; 19 | 20 | 21 | Button run_button; 22 | EditText command_input_edit_text; 23 | TextView command_output_text_view; 24 | String mInputCommand =""; 25 | 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_main); 31 | 32 | command_input_edit_text = findViewById(R.id.edit_text); 33 | run_button = findViewById(R.id.button); 34 | command_output_text_view = findViewById(R.id.text_view); 35 | setCommandOutputTextViewText("Enter apk input and out file path separated with a space"); 36 | 37 | run_button.setOnClickListener(v -> { 38 | mInputCommand = command_input_edit_text.getText().toString(); 39 | setCommandOutputTextViewText("$ " + mInputCommand); 40 | runCommand(); 41 | }); 42 | 43 | BootstrapInstaller.installBootstrap(this); 44 | } 45 | 46 | private synchronized void runCommand() { 47 | String[] pathArgs = mInputCommand.split("\\s+"); 48 | setCommandOutputTextViewText(""); 49 | if (pathArgs.length != 2) { 50 | setCommandOutputTextViewText("Enter apk input and out file path separated with a space"); 51 | return; 52 | } 53 | 54 | if (executing) { 55 | Logger.showToast(this, "Already executing command", false); 56 | return; 57 | } 58 | 59 | executing = true; 60 | 61 | new Thread() { 62 | @Override 63 | public void run() { 64 | StringBuilder commandMarkdownOutput = new StringBuilder(); 65 | ApkTools.processApk(MainActivity.this, pathArgs[0], pathArgs[1], false, commandMarkdownOutput); 66 | runOnUiThread(() -> setCommandOutputTextViewText(commandMarkdownOutput.toString())); 67 | executing = false; 68 | } 69 | }.start(); 70 | } 71 | 72 | private void setCommandOutputTextViewText(String text) { 73 | if (command_output_text_view != null) 74 | command_output_text_view.setText(text); 75 | } 76 | 77 | private void appendCommandOutputTextViewText(String text) { 78 | if (command_output_text_view != null) 79 | command_output_text_view.append(text); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/ApkTools.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import net.dinglisch.android.appfactory.AppFactoryConstants; 9 | import net.dinglisch.android.appfactory.utils.errors.Error; 10 | import net.dinglisch.android.appfactory.utils.file.FileUtils; 11 | import net.dinglisch.android.appfactory.utils.logger.Logger; 12 | import net.dinglisch.android.appfactory.utils.markdown.MarkdownUtils; 13 | import net.dinglisch.android.appfactory.utils.shell.ExecutionCommand; 14 | import net.dinglisch.android.appfactory.utils.shell.environment.AppShellEnvironment; 15 | import net.dinglisch.android.appfactory.utils.shell.runner.app.AppShell; 16 | 17 | import java.io.File; 18 | import java.text.SimpleDateFormat; 19 | import java.util.Date; 20 | import java.util.TimeZone; 21 | 22 | public class ApkTools { 23 | 24 | private static final String LOG_TAG = "ApkTools"; 25 | 26 | public static boolean processApk(@NonNull final Context context, 27 | @NonNull String apkInputFilePath, 28 | @NonNull String apkOutputFilePath, 29 | boolean verbose, 30 | @NonNull StringBuilder commandMarkdownOutput) { 31 | File apkInputFile = new File(apkInputFilePath); 32 | String processingDirectoryPath = AppFactoryConstants.TMP_PREFIX_DIR_PATH + "/" + "apk-" + getCurrentMilliSecondLocalTimeStamp(); 33 | File processingDirectory = new File(processingDirectoryPath); 34 | String verboseFlags; 35 | 36 | String error = null; 37 | apkInputFilePath = FileUtils.getCanonicalPath(apkInputFilePath, null); 38 | if (!apkInputFilePath.endsWith(".apk")) { 39 | error = "The APK input file path \"" + apkInputFilePath + "\" does not end with \".apk\" extension"; 40 | } else if (apkInputFilePath.contains("'")) { 41 | error = "The APK input file path \"" + apkInputFilePath + "\" contains single quotes"; 42 | } else if (apkOutputFilePath.contains("'")) { 43 | error = "The APK output file path \"" + apkOutputFilePath + "\" contains single quotes"; 44 | } else if (!apkInputFile.exists() || !apkInputFile.isFile()) { 45 | error = "An APK file not found at \"" + apkInputFilePath + "\""; 46 | } else if (!processingDirectory.mkdirs() || !processingDirectory.isDirectory()) { 47 | error = "Failed to create APK processing directory \"" + processingDirectoryPath + "\""; 48 | } 49 | 50 | if (error != null) { 51 | Logger.logErrorExtended(LOG_TAG, error); 52 | commandMarkdownOutput.append(getMarkdownCommandOutput("Process APK", error)); 53 | return false; 54 | } 55 | 56 | String apkFileBasenameWithoutExtension = FileUtils.getFileBasenameWithoutExtension(apkInputFilePath); 57 | 58 | // Decompress resources.arcs in APK 59 | String apkDecompressedFilePath = processingDirectoryPath + "/" + apkFileBasenameWithoutExtension + "-uncompressed.apk"; 60 | verboseFlags = verbose ? "" : " -q"; 61 | String script = "{ " + "cd '" + processingDirectoryPath + "' && unzip" + verboseFlags + " -o '" + apkInputFilePath + "' -d unzip && cd unzip && zip -n 'resources.arsc'" + verboseFlags + " -r '" + apkDecompressedFilePath + "' * ; ret=$?; [ $ret -eq 0 ] && echo success || echo failed; exit $ret; } 2>&1"; 62 | if (!runAppShellCommand(context, "Decompress resource.arsc in APK", script, commandMarkdownOutput)) { 63 | deleteDirectory(processingDirectoryPath); 64 | return false; 65 | } 66 | 67 | // Zipalign APK 68 | verboseFlags = verbose ? " -v" : ""; 69 | String apkZipalignedFilePath = processingDirectoryPath + "/" + apkFileBasenameWithoutExtension + "-zipaligned.apk"; 70 | script = "{ " + "cd '" + processingDirectoryPath + "' && zipalign" + verboseFlags + " 4 '" + apkDecompressedFilePath + "' '" + apkZipalignedFilePath + "'; ret=$?; [ $ret -eq 0 ] && echo success || echo failed; exit $ret; } 2>&1"; 71 | if (!runAppShellCommand(context, "Zipalign APK", script, commandMarkdownOutput)) { 72 | deleteDirectory(processingDirectoryPath); 73 | return false; 74 | } 75 | 76 | // Sign APK 77 | String apkSignedFilePath = processingDirectoryPath + "/" + apkFileBasenameWithoutExtension + "-signed.apk"; 78 | apkSignedFilePath = apkZipalignedFilePath; 79 | 80 | script = "{ " + "/system/bin/mv '" + apkSignedFilePath + "' '" + apkOutputFilePath + "'; ret=$?; [ $ret -eq 0 ] && echo success || echo failed; exit $ret; } 2>&1"; 81 | if (!runAppShellCommand(context, "Move APK to output path", script, commandMarkdownOutput)) { 82 | deleteDirectory(processingDirectoryPath); 83 | return false; 84 | } 85 | 86 | // Cleanup processing directory 87 | deleteDirectory(processingDirectoryPath); 88 | 89 | return true; 90 | } 91 | 92 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 93 | public static boolean runAppShellCommand(@NonNull final Context context, 94 | @NonNull String label, 95 | @NonNull String script, 96 | @NonNull StringBuilder commandMarkdownOutput) { 97 | // Run script 98 | ExecutionCommand executionCommand = new ExecutionCommand(-1, "/system/bin/sh", 99 | null, script + "\n", null, ExecutionCommand.Runner.APP_SHELL.getName(), false); 100 | executionCommand.commandLabel = label; 101 | AppShell appShell = AppShell.execute(context, executionCommand, null, 102 | new AppShellEnvironment(context), null, true); 103 | if (appShell == null || !executionCommand.isSuccessful()) { 104 | Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); 105 | commandMarkdownOutput.append(getMarkdownCommandOutput(label, executionCommand.toString())); 106 | return false; 107 | } 108 | 109 | // Build script output 110 | StringBuilder commandOutput = new StringBuilder(); 111 | commandOutput.append("$ ").append(script); 112 | commandOutput.append("\n").append(executionCommand.resultData.stdout); 113 | 114 | boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty(); 115 | if (executionCommand.resultData.exitCode != 0 || stderrSet) { 116 | Logger.logErrorExtended(LOG_TAG, executionCommand.toString()); 117 | if (stderrSet) 118 | commandOutput.append("\n").append(executionCommand.resultData.stderr); 119 | commandOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString()); 120 | } 121 | 122 | commandMarkdownOutput.append(getMarkdownCommandOutput(label, commandOutput.toString())); 123 | 124 | return executionCommand.resultData.exitCode == 0; 125 | } 126 | 127 | private static void deleteDirectory(@NonNull String directoryPath) { 128 | // Use your own function if you don't want to use FileUtils, although it is safe and tested. 129 | Error error = FileUtils.deleteDirectoryFile("APK processing directory", directoryPath, true); 130 | if (error != null) { 131 | Logger.logError(LOG_TAG, error.getErrorLogString()); 132 | } 133 | } 134 | 135 | private static String getMarkdownCommandOutput(@NonNull String label, String commandOutput) { 136 | StringBuilder commandMarkdownOutput = new StringBuilder(); 137 | // Build markdown output 138 | commandMarkdownOutput.append("## ").append(label).append("\n\n"); 139 | commandMarkdownOutput.append(MarkdownUtils.getMarkdownCodeForString(commandOutput, true)); 140 | commandMarkdownOutput.append("\n##\n\n\n"); 141 | return commandMarkdownOutput.toString(); 142 | } 143 | 144 | 145 | public static String getCurrentMilliSecondLocalTimeStamp() { 146 | @SuppressLint("SimpleDateFormat") 147 | final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss.SSS"); 148 | df.setTimeZone(TimeZone.getDefault()); 149 | return df.format(new Date()); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/BootstrapInstaller.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.system.Os; 6 | 7 | import androidx.annotation.Nullable; 8 | 9 | import net.dinglisch.android.appfactory.utils.errors.Error; 10 | import net.dinglisch.android.appfactory.utils.file.FileUtils; 11 | import net.dinglisch.android.appfactory.utils.logger.Logger; 12 | 13 | import java.io.BufferedReader; 14 | import java.io.File; 15 | import java.io.FileReader; 16 | 17 | public class BootstrapInstaller { 18 | 19 | private static final String LOG_TAG = "BootstrapInstaller"; 20 | 21 | private static final String FILES_MAPPING_FILE = "libfiles.so"; 22 | private static final String SYMLINKS_MAPPING_FILE = "libsymlinks.so"; 23 | 24 | public static boolean installBootstrap(Context context) { 25 | try { 26 | ApplicationInfo applicationInfo = context.getApplicationInfo(); 27 | 28 | String prefixDirectoryPath = context.getFilesDir() + "/usr"; 29 | 30 | Error error = FileUtils.deleteDirectoryFile("prefix directory", prefixDirectoryPath, true); 31 | if (error != null) { 32 | Logger.logError(LOG_TAG, error.getErrorLogString()); 33 | return false; 34 | } 35 | 36 | File filesMappingFile = new File(applicationInfo.nativeLibraryDir, FILES_MAPPING_FILE); 37 | if (!filesMappingFile.exists()) { 38 | Logger.logError(LOG_TAG, "No FILES_MAPPING_FILE found at \"" + filesMappingFile.getAbsolutePath() +"\""); 39 | return false; 40 | } 41 | 42 | Logger.logError(LOG_TAG, "Installing bootstrap"); 43 | BufferedReader reader = new BufferedReader(new FileReader(filesMappingFile)); 44 | String line; 45 | while ((line = reader.readLine()) != null) { 46 | String[] parts = line.split("←"); 47 | if (parts.length != 2) { 48 | Logger.logError(LOG_TAG, "Malformed line " + line + " in FILES_MAPPING_FILE \"" + filesMappingFile.getAbsolutePath() + "\""); 49 | continue; 50 | } 51 | 52 | String oldPath = applicationInfo.nativeLibraryDir + "/" + parts[0]; 53 | String newPath = prefixDirectoryPath + "/" + parts[1]; 54 | 55 | if (!ensureDirectoryExists(new File(newPath).getParentFile())) 56 | return false; 57 | 58 | Logger.logVerbose(LOG_TAG, "About to setup link: \"" + oldPath + "\" ← \"" + newPath + "\""); 59 | error = FileUtils.deleteFile("link destination", newPath, true); 60 | if (error != null) { 61 | Logger.logError(LOG_TAG, error.getErrorLogString()); 62 | return false; 63 | } 64 | 65 | Os.symlink(oldPath, newPath); 66 | } 67 | 68 | File symlinksMappingFile = new File(applicationInfo.nativeLibraryDir, SYMLINKS_MAPPING_FILE); 69 | if (!symlinksMappingFile.exists()) { 70 | Logger.logError(LOG_TAG, "No SYMLINKS_MAPPING_FILE found at \"" + symlinksMappingFile.getAbsolutePath() + "\""); 71 | } 72 | 73 | reader = new BufferedReader(new FileReader(symlinksMappingFile)); 74 | while ((line = reader.readLine()) != null) { 75 | String[] parts = line.split("←"); 76 | if (parts.length != 2) { 77 | Logger.logError(LOG_TAG, "Malformed line " + line + " in SYMLINKS_MAPPING_FILE \"" + symlinksMappingFile.getAbsolutePath() + "\""); 78 | continue; 79 | } 80 | 81 | String oldPath = parts[0]; 82 | String newPath = prefixDirectoryPath + "/" + parts[1]; 83 | 84 | if (!ensureDirectoryExists(new File(newPath).getParentFile())) 85 | return false; 86 | 87 | Logger.logVerbose(LOG_TAG, "About to setup link: \"" + oldPath + "\" ← \"" + newPath + "\""); 88 | error = FileUtils.deleteFile("link destination", newPath, true); 89 | if (error != null) { 90 | Logger.logError(LOG_TAG, error.getErrorLogString()); 91 | return false; 92 | } 93 | Os.symlink(oldPath, newPath); 94 | } 95 | } catch (Throwable t) { 96 | Logger.logStackTraceWithMessage(LOG_TAG, "Failed to setup bootstrap", t); 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 104 | private static boolean ensureDirectoryExists(@Nullable File directory) { 105 | /* 106 | Error error = FileUtils.createDirectoryFile(directory != null ? directory.getAbsolutePath() : null); 107 | if (error != null) { 108 | Logger.logError(LOG_TAG, error.getErrorLogString()); 109 | return false; 110 | } 111 | */ 112 | 113 | if (directory == null) 114 | return true; 115 | if (!directory.isDirectory() && !directory.mkdirs()) { 116 | Logger.logError(LOG_TAG, "Unable to create directory at \"" + directory.getAbsolutePath() + "\""); 117 | return false; 118 | } 119 | return true; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/android/PackageUtils.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.android; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.os.Build; 6 | import android.os.UserHandle; 7 | import android.os.UserManager; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.annotation.RequiresApi; 12 | 13 | public class PackageUtils { 14 | 15 | private static final String LOG_TAG = "PackageUtils"; 16 | 17 | /** 18 | * Get the uid for the package associated with the {@code context}. 19 | * 20 | * @param context The {@link Context} for the package. 21 | * @return Returns the uid. 22 | */ 23 | public static int getUidForPackage(@NonNull final Context context) { 24 | return getUidForPackage(context.getApplicationInfo()); 25 | } 26 | 27 | /** 28 | * Get the uid for the package associated with the {@code applicationInfo}. 29 | * 30 | * @param applicationInfo The {@link ApplicationInfo} for the package. 31 | * @return Returns the uid. 32 | */ 33 | public static int getUidForPackage(@NonNull final ApplicationInfo applicationInfo) { 34 | return applicationInfo.uid; 35 | } 36 | 37 | /** 38 | * Get the serial number for the user for the package associated with the {@code context}. 39 | * 40 | * @param context The {@link Context} for the package. 41 | * @return Returns the serial number. This will be {@code null} if failed to get it. 42 | */ 43 | @RequiresApi(api = Build.VERSION_CODES.N) 44 | @Nullable 45 | public static Long getUserIdForPackage(@NonNull Context context) { 46 | UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 47 | if (userManager == null) return null; 48 | return userManager.getSerialNumberForUser(UserHandle.getUserHandleForUid(getUidForPackage(context))); 49 | } 50 | 51 | /** 52 | * Check if the current user is the primary user. This is done by checking if the the serial 53 | * number for the current user equals 0. 54 | * 55 | * @param context The {@link Context} for operations. 56 | * @return Returns {@code true} if the current user is the primary user, otherwise [@code false}. 57 | */ 58 | @RequiresApi(api = Build.VERSION_CODES.N) 59 | public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) { 60 | Long userId = getUserIdForPackage(context); 61 | return userId != null && userId == 0; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/data/DataUtils.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.data; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import com.google.common.base.Strings; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.ObjectOutputStream; 12 | import java.io.Serializable; 13 | 14 | public class DataUtils { 15 | 16 | /** Max safe limit of data size to prevent TransactionTooLargeException when transferring data 17 | * inside or to other apps via transactions. */ 18 | public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB 19 | 20 | private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); 21 | 22 | public static String getTruncatedCommandOutput(String text, int maxLength, boolean fromEnd, boolean onNewline, boolean addPrefix) { 23 | if (text == null) return null; 24 | 25 | String prefix = "(truncated) "; 26 | 27 | if (addPrefix) 28 | maxLength = maxLength - prefix.length(); 29 | 30 | if (maxLength < 0 || text.length() < maxLength) return text; 31 | 32 | if (fromEnd) { 33 | text = text.substring(0, maxLength); 34 | } else { 35 | int cutOffIndex = text.length() - maxLength; 36 | 37 | if (onNewline) { 38 | int nextNewlineIndex = text.indexOf('\n', cutOffIndex); 39 | if (nextNewlineIndex != -1 && nextNewlineIndex != text.length() - 1) { 40 | cutOffIndex = nextNewlineIndex + 1; 41 | } 42 | } 43 | text = text.substring(cutOffIndex); 44 | } 45 | 46 | if (addPrefix) 47 | text = prefix + text; 48 | 49 | return text; 50 | } 51 | 52 | 53 | 54 | /** 55 | * Get the object itself if it is not {@code null}, otherwise default. 56 | * 57 | * @param object The {@link Object} to check. 58 | * @param def The default {@link Object}. 59 | * @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}. 60 | */ 61 | public static T getDefaultIfNull(@Nullable T object, @Nullable T def) { 62 | return (object == null) ? def : object; 63 | } 64 | 65 | /** 66 | * Get the {@link String} itself if it is not {@code null} or empty, otherwise default. 67 | * 68 | * @param value The {@link String} to check. 69 | * @param def The default {@link String}. 70 | * @return Returns {@code value} if it is not {@code null} or empty, otherwise returns {@code def}. 71 | */ 72 | public static String getDefaultIfUnset(@Nullable String value, String def) { 73 | return (value == null || value.isEmpty()) ? def : value; 74 | } 75 | 76 | /** Check if a string is null or empty. */ 77 | public static boolean isNullOrEmpty(String string) { 78 | return string == null || string.isEmpty(); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/errors/Errno.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.errors; 2 | 3 | import android.app.Activity; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import net.dinglisch.android.appfactory.utils.logger.Logger; 8 | 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | 14 | /** The {@link Class} that defines error messages and codes. */ 15 | public class Errno { 16 | 17 | private static final HashMap map = new HashMap<>(); 18 | 19 | public static final String TYPE = "Error"; 20 | 21 | 22 | public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success"); 23 | public static final Errno ERRNO_CANCELLED = new Errno(TYPE, Activity.RESULT_CANCELED, "Cancelled"); 24 | public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure"); 25 | public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed"); 26 | 27 | /** The errno type. */ 28 | protected final String type; 29 | /** The errno code. */ 30 | protected final int code; 31 | /** The errno message. */ 32 | protected final String message; 33 | 34 | private static final String LOG_TAG = "Errno"; 35 | 36 | 37 | public Errno(@NonNull final String type, final int code, @NonNull final String message) { 38 | this.type = type; 39 | this.code = code; 40 | this.message = message; 41 | map.put(type + ":" + code, this); 42 | } 43 | 44 | @NonNull 45 | @Override 46 | public String toString() { 47 | return "type=" + type + ", code=" + code + ", message=\"" + message + "\""; 48 | } 49 | 50 | @NonNull 51 | public String getType() { 52 | return type; 53 | } 54 | 55 | public int getCode() { 56 | return code; 57 | } 58 | 59 | @NonNull 60 | public String getMessage() { 61 | return message; 62 | } 63 | 64 | 65 | 66 | /** 67 | * Get the {@link Errno} of a specific type and code. 68 | * 69 | * @param type The unique type of the {@link Errno}. 70 | * @param code The unique code of the {@link Errno}. 71 | */ 72 | public static Errno valueOf(String type, Integer code) { 73 | if (type == null || type.isEmpty() || code == null) return null; 74 | return map.get(type + ":" + code); 75 | } 76 | 77 | 78 | 79 | public Error getError() { 80 | return new Error(getType(), getCode(), getMessage()); 81 | } 82 | 83 | public Error getError(Object... args) { 84 | try { 85 | return new Error(getType(), getCode(), String.format(getMessage(), args)); 86 | } catch (Exception e) { 87 | Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage()); 88 | // Return unformatted message as a backup 89 | return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args)); 90 | } 91 | } 92 | 93 | public Error getError(Throwable throwable, Object... args) { 94 | if (throwable == null) 95 | return getError(args); 96 | else 97 | return getError(Collections.singletonList(throwable), args); 98 | } 99 | 100 | public Error getError(List throwablesList, Object... args) { 101 | try { 102 | if (throwablesList == null) 103 | return new Error(getType(), getCode(), String.format(getMessage(), args)); 104 | else 105 | return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList); 106 | } catch (Exception e) { 107 | Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage()); 108 | // Return unformatted message as a backup 109 | return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args), throwablesList); 110 | } 111 | } 112 | 113 | public boolean equalsErrorTypeAndCode(Error error) { 114 | if (error == null) return false; 115 | return type.equals(error.getType()) && code == error.getCode(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/errors/Error.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.errors; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import net.dinglisch.android.appfactory.utils.logger.Logger; 8 | import net.dinglisch.android.appfactory.utils.markdown.MarkdownUtils; 9 | 10 | import java.io.Serializable; 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | public class Error implements Serializable { 16 | 17 | /** The optional error label. */ 18 | private String label; 19 | /** The error type. */ 20 | private String type; 21 | /** The error code. */ 22 | private int code; 23 | /** The error message. */ 24 | private String message; 25 | /** The error exceptions. */ 26 | private List throwablesList = new ArrayList<>(); 27 | 28 | private static final String LOG_TAG = "Error"; 29 | 30 | 31 | public Error() { 32 | InitError(null, null, null, null); 33 | } 34 | 35 | public Error(String type, Integer code, String message, List throwablesList) { 36 | InitError(type, code, message, throwablesList); 37 | } 38 | 39 | public Error(String type, Integer code, String message, Throwable throwable) { 40 | InitError(type, code, message, Collections.singletonList(throwable)); 41 | } 42 | 43 | public Error(String type, Integer code, String message) { 44 | InitError(type, code, message, null); 45 | } 46 | 47 | public Error(Integer code, String message, List throwablesList) { 48 | InitError(null, code, message, throwablesList); 49 | } 50 | 51 | public Error(Integer code, String message, Throwable throwable) { 52 | InitError(null, code, message, Collections.singletonList(throwable)); 53 | } 54 | 55 | public Error(Integer code, String message) { 56 | InitError(null, code, message, null); 57 | } 58 | 59 | public Error(String message, Throwable throwable) { 60 | InitError(null, null, message, Collections.singletonList(throwable)); 61 | } 62 | 63 | public Error(String message, List throwablesList) { 64 | InitError(null, null, message, throwablesList); 65 | } 66 | 67 | public Error(String message) { 68 | InitError(null, null, message, null); 69 | } 70 | 71 | private void InitError(String type, Integer code, String message, List throwablesList) { 72 | if (type != null && !type.isEmpty()) 73 | this.type = type; 74 | else 75 | this.type = Errno.TYPE; 76 | 77 | if (code != null && code > Errno.ERRNO_SUCCESS.getCode()) 78 | this.code = code; 79 | else 80 | this.code = Errno.ERRNO_SUCCESS.getCode(); 81 | 82 | this.message = message; 83 | 84 | if (throwablesList != null) 85 | this.throwablesList = throwablesList; 86 | } 87 | 88 | public Error setLabel(String label) { 89 | this.label = label; 90 | return this; 91 | } 92 | 93 | public String getLabel() { 94 | return label; 95 | } 96 | 97 | 98 | public String getType() { 99 | return type; 100 | } 101 | 102 | public Integer getCode() { 103 | return code; 104 | } 105 | 106 | public String getMessage() { 107 | return message; 108 | } 109 | 110 | public void prependMessage(String message) { 111 | if (message != null && isStateFailed()) 112 | this.message = message + this.message; 113 | } 114 | 115 | public void appendMessage(String message) { 116 | if (message != null && isStateFailed()) 117 | this.message = this.message + message; 118 | } 119 | 120 | public List getThrowablesList() { 121 | return Collections.unmodifiableList(throwablesList); 122 | } 123 | 124 | 125 | public synchronized boolean setStateFailed(@NonNull Error error) { 126 | return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null); 127 | } 128 | 129 | public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) { 130 | return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable)); 131 | } 132 | public synchronized boolean setStateFailed(@NonNull Error error, List throwablesList) { 133 | return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList); 134 | } 135 | 136 | public synchronized boolean setStateFailed(int code, String message) { 137 | return setStateFailed(this.type, code, message, null); 138 | } 139 | 140 | public synchronized boolean setStateFailed(int code, String message, Throwable throwable) { 141 | return setStateFailed(this.type, code, message, Collections.singletonList(throwable)); 142 | } 143 | 144 | public synchronized boolean setStateFailed(int code, String message, List throwablesList) { 145 | return setStateFailed(this.type, code, message, throwablesList); 146 | } 147 | 148 | public synchronized boolean setStateFailed(String type, int code, String message, List throwablesList) { 149 | this.message = message; 150 | this.throwablesList = throwablesList; 151 | 152 | if (type != null && !type.isEmpty()) 153 | this.type = type; 154 | 155 | if (code > Errno.ERRNO_SUCCESS.getCode()) { 156 | this.code = code; 157 | return true; 158 | } else { 159 | Logger.logWarn(LOG_TAG, "Ignoring invalid error code value \"" + code + "\". Force setting it to RESULT_CODE_FAILED \"" + Errno.ERRNO_FAILED.getCode() + "\""); 160 | this.code = Errno.ERRNO_FAILED.getCode(); 161 | return false; 162 | } 163 | } 164 | 165 | public boolean isStateFailed() { 166 | return code > Errno.ERRNO_SUCCESS.getCode(); 167 | } 168 | 169 | 170 | @NonNull 171 | @Override 172 | public String toString() { 173 | return getErrorLogString(this); 174 | } 175 | 176 | 177 | 178 | /** 179 | * Log the {@link Error} and show a toast for the minimal {@link String} for the {@link Error}. 180 | * 181 | * @param context The {@link Context} for operations. 182 | * @param logTag The log tag to use for logging. 183 | * @param error The {@link Error} to convert. 184 | */ 185 | public static void logErrorAndShowToast(Context context, String logTag, Error error) { 186 | if (error == null) return; 187 | error.logErrorAndShowToast(context, logTag); 188 | } 189 | 190 | public void logErrorAndShowToast(Context context, String logTag) { 191 | Logger.logErrorExtended(logTag, getErrorLogString()); 192 | Logger.showToast(context, getMinimalErrorLogString(), true); 193 | } 194 | 195 | 196 | /** 197 | * Get a log friendly {@link String} for {@link Error} error parameters. 198 | * 199 | * @param error The {@link Error} to convert. 200 | * @return Returns the log friendly {@link String}. 201 | */ 202 | public static String getErrorLogString(final Error error) { 203 | if (error == null) return "null"; 204 | return error.getErrorLogString(); 205 | } 206 | 207 | public String getErrorLogString() { 208 | StringBuilder logString = new StringBuilder(); 209 | 210 | logString.append(getCodeString()); 211 | logString.append("\n").append(getTypeAndMessageLogString()); 212 | if (throwablesList != null && throwablesList.size() > 0) 213 | logString.append("\n").append(geStackTracesLogString()); 214 | 215 | return logString.toString(); 216 | } 217 | 218 | /** 219 | * Get a minimal log friendly {@link String} for {@link Error} error parameters. 220 | * 221 | * @param error The {@link Error} to convert. 222 | * @return Returns the log friendly {@link String}. 223 | */ 224 | public static String getMinimalErrorLogString(final Error error) { 225 | if (error == null) return "null"; 226 | return error.getMinimalErrorLogString(); 227 | } 228 | 229 | public String getMinimalErrorLogString() { 230 | StringBuilder logString = new StringBuilder(); 231 | 232 | logString.append(getCodeString()); 233 | logString.append(getTypeAndMessageLogString()); 234 | 235 | return logString.toString(); 236 | } 237 | 238 | /** 239 | * Get a minimal {@link String} for {@link Error} error parameters. 240 | * 241 | * @param error The {@link Error} to convert. 242 | * @return Returns the {@link String}. 243 | */ 244 | public static String getMinimalErrorString(final Error error) { 245 | if (error == null) return "null"; 246 | return error.getMinimalErrorString(); 247 | } 248 | 249 | public String getMinimalErrorString() { 250 | StringBuilder logString = new StringBuilder(); 251 | 252 | logString.append("(").append(getCode()).append(") "); 253 | logString.append(getType()).append(": ").append(getMessage()); 254 | 255 | return logString.toString(); 256 | } 257 | 258 | /** 259 | * Get a markdown {@link String} for {@link Error}. 260 | * 261 | * @param error The {@link Error} to convert. 262 | * @return Returns the markdown {@link String}. 263 | */ 264 | public static String getErrorMarkdownString(final Error error) { 265 | if (error == null) return "null"; 266 | return error.getErrorMarkdownString(); 267 | } 268 | 269 | public String getErrorMarkdownString() { 270 | StringBuilder markdownString = new StringBuilder(); 271 | 272 | markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", getCode(), "-")); 273 | markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry( 274 | (Errno.TYPE.equals(getType()) ? "Error Message" : "Error Message (" + getType() + ")"), message, "-")); 275 | if (throwablesList != null && throwablesList.size() > 0) 276 | markdownString.append("\n\n").append(geStackTracesMarkdownString()); 277 | 278 | return markdownString.toString(); 279 | } 280 | 281 | 282 | public String getCodeString() { 283 | return Logger.getSingleLineLogStringEntry("Error Code", code, "-"); 284 | } 285 | 286 | public String getTypeAndMessageLogString() { 287 | return Logger.getMultiLineLogStringEntry(Errno.TYPE.equals(type) ? "Error Message" : "Error Message (" + type + ")", message, "-"); 288 | } 289 | 290 | public String geStackTracesLogString() { 291 | return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList)); 292 | } 293 | 294 | public String geStackTracesMarkdownString() { 295 | return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList)); 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/errors/FunctionErrno.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.errors; 2 | 3 | /** The {@link Class} that defines function error messages and codes. */ 4 | public class FunctionErrno extends Errno { 5 | 6 | public static final String TYPE = "Function Error"; 7 | 8 | 9 | /* Errors for null or empty parameters (100-150) */ 10 | public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETER = new Errno(TYPE, 100, "The %1$s parameter passed to \"%2$s\" is null or empty."); 11 | public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty."); 12 | public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set."); 13 | public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set."); 14 | public static final Errno ERRNO_INVALID_PARAMETER = new Errno(TYPE, 104, "The %1$s parameter passed to \"%2$s\" is invalid.\"%3$s\""); 15 | public static final Errno ERRNO_PARAMETER_NOT_INSTANCE_OF = new Errno(TYPE, 104, "The %1$s parameter passed to \"%2$s\" is not an instance of %3$s."); 16 | 17 | 18 | FunctionErrno(final String type, final int code, final String message) { 19 | super(type, code, message); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/FileUtilsErrno.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.file; 2 | 3 | import net.dinglisch.android.appfactory.utils.errors.Errno; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** The {@link Class} that defines FileUtils error messages and codes. */ 9 | public class FileUtilsErrno extends Errno { 10 | 11 | public static final String TYPE = "FileUtils Error"; 12 | 13 | 14 | /* Errors for null or empty paths (100-150) */ 15 | public static final Errno ERRNO_EXECUTABLE_REQUIRED = new Errno(TYPE, 100, "Executable required."); 16 | public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE_PATH = new Errno(TYPE, 101, "The regular file path is null or empty."); 17 | public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE = new Errno(TYPE, 102, "The regular file is null or empty."); 18 | public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE_PATH = new Errno(TYPE, 103, "The executable file path is null or empty."); 19 | public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE = new Errno(TYPE, 104, "The executable file is null or empty."); 20 | public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE_PATH = new Errno(TYPE, 105, "The directory file path is null or empty."); 21 | public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 106, "The directory file is null or empty."); 22 | 23 | 24 | 25 | /* Errors for invalid or not found files at path (150-200) */ 26 | public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s not found at path \"%2$s\"."); 27 | public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT = new Errno(TYPE, 151, "The %1$s not found at path."); 28 | 29 | public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 152, "Non-regular file found at %1$s path \"%2$s\"."); 30 | public static final Errno ERRNO_NON_REGULAR_FILE_FOUND_SHORT = new Errno(TYPE, 153, "Non-regular file found at %1$s path."); 31 | public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path \"%2$s\"."); 32 | public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT = new Errno(TYPE, 155, "Non-directory file found at %1$s path."); 33 | public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 156, "Non-symlink file found at %1$s path \"%2$s\"."); 34 | public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path."); 35 | 36 | public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" of type \"%3$s\" is not one of allowed file types \"%4$s\"."); 37 | public static final Errno ERRNO_NON_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 159, "The %1$s directory at path \"%2$s\" is not empty."); 38 | 39 | public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s"); 40 | public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 161, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s"); 41 | public static final Errno ERRNO_VALIDATE_DIRECTORY_EMPTY_OR_ONLY_CONTAINS_SPECIFIC_FILES_FAILED_WITH_EXCEPTION = new Errno(TYPE, 162, "Validating directory is empty or only contains specific files of %1$s at path \"%2$s\" failed.\nException: %3$s"); 42 | 43 | 44 | 45 | /* Errors for file creation (200-250) */ 46 | public static final Errno ERRNO_CREATING_FILE_FAILED = new Errno(TYPE, 200, "Creating %1$s at path \"%2$s\" failed."); 47 | public static final Errno ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 201, "Creating %1$s at path \"%2$s\" failed.\nException: %3$s"); 48 | 49 | public static final Errno ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE = new Errno(TYPE, 202, "Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink."); 50 | public static final Errno ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s"); 51 | 52 | 53 | 54 | /* Errors for file copying and moving (250-300) */ 55 | public static final Errno ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 250, "%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s"); 56 | public static final Errno ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH = new Errno(TYPE, 251, "%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path."); 57 | public static final Errno ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE = new Errno(TYPE, 252, "Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\"."); 58 | public static final Errno ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF = new Errno(TYPE, 253, "Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source."); 59 | 60 | 61 | 62 | /* Errors for file deletion (300-350) */ 63 | public static final Errno ERRNO_DELETING_FILE_FAILED = new Errno(TYPE, 300, "Deleting %1$s at path \"%2$s\" failed."); 64 | public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s"); 65 | public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s"); 66 | public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\"."); 67 | public static final Errno ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 304, "Deleting %1$s under directory at path \"%2$s\" old than %3$s days failed.\nException: %4$s"); 68 | 69 | 70 | 71 | /* Errors for file reading and writing (350-400) */ 72 | public static final Errno ERRNO_READING_TEXT_FROM_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 350, "Reading text from %1$s at path \"%2$s\" failed.\nException: %3$s"); 73 | public static final Errno ERRNO_WRITING_TEXT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing text to %1$s at path \"%2$s\" failed.\nException: %3$s"); 74 | public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\""); 75 | public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s"); 76 | public static final Errno ERRNO_GET_CHARSET_FOR_NAME_FAILED = new Errno(TYPE, 354, "The \"%1$s\" charset is not supported.\nException: %2$s"); 77 | public static final Errno ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 355, "Reading serializable object from %1$s at path \"%2$s\" failed.\nException: %3$s"); 78 | public static final Errno ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 356, "Writing serializable object to %1$s at path \"%2$s\" failed.\nException: %3$s"); 79 | 80 | 81 | 82 | /* Errors for invalid file permissions (400-450) */ 83 | public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid."); 84 | public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path \"%2$s\" is not readable. Permission Denied."); 85 | public static final Errno ERRNO_FILE_NOT_READABLE_SHORT = new Errno(TYPE, 402, "The %1$s at path is not readable. Permission Denied."); 86 | public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 403, "The %1$s at path \"%2$s\" is not writable. Permission Denied."); 87 | public static final Errno ERRNO_FILE_NOT_WRITABLE_SHORT = new Errno(TYPE, 404, "The %1$s at path is not writable. Permission Denied."); 88 | public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 405, "The %1$s at path \"%2$s\" is not executable. Permission Denied."); 89 | public static final Errno ERRNO_FILE_NOT_EXECUTABLE_SHORT = new Errno(TYPE, 406, "The %1$s at path is not executable. Permission Denied."); 90 | 91 | 92 | FileUtilsErrno(final String type, final int code, final String message) { 93 | super(type, code, message); 94 | } 95 | 96 | 97 | 98 | /** Defines the {@link Errno} mapping to get a shorter version of {@link FileUtilsErrno}. */ 99 | public static final Map ERRNO_SHORT_MAPPING = new HashMap() {{ 100 | put(ERRNO_FILE_NOT_FOUND_AT_PATH, ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT); 101 | 102 | put(ERRNO_NON_REGULAR_FILE_FOUND, ERRNO_NON_REGULAR_FILE_FOUND_SHORT); 103 | put(ERRNO_NON_DIRECTORY_FILE_FOUND, ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT); 104 | put(ERRNO_NON_SYMLINK_FILE_FOUND, ERRNO_NON_SYMLINK_FILE_FOUND_SHORT); 105 | 106 | put(ERRNO_FILE_NOT_READABLE, ERRNO_FILE_NOT_READABLE_SHORT); 107 | put(ERRNO_FILE_NOT_WRITABLE, ERRNO_FILE_NOT_WRITABLE_SHORT); 108 | put(ERRNO_FILE_NOT_EXECUTABLE, ERRNO_FILE_NOT_EXECUTABLE_SHORT); 109 | }}; 110 | 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FileAttributes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Oracle designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Oracle in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 | * or visit www.oracle.com if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | package net.dinglisch.android.appfactory.utils.file.filesystem; 27 | 28 | import android.os.Build; 29 | import android.system.StructStat; 30 | 31 | import androidx.annotation.NonNull; 32 | 33 | import java.io.File; 34 | import java.io.FileDescriptor; 35 | import java.io.IOException; 36 | import java.util.HashSet; 37 | import java.util.Set; 38 | import java.util.concurrent.TimeUnit; 39 | 40 | /** 41 | * Unix implementation of PosixFileAttributes. 42 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileAttributes.java 43 | */ 44 | 45 | public class FileAttributes { 46 | private String filePath; 47 | private FileDescriptor fileDescriptor; 48 | 49 | private int st_mode; 50 | private long st_ino; 51 | private long st_dev; 52 | private long st_rdev; 53 | private long st_nlink; 54 | private int st_uid; 55 | private int st_gid; 56 | private long st_size; 57 | private long st_blksize; 58 | private long st_blocks; 59 | private long st_atime_sec; 60 | private long st_atime_nsec; 61 | private long st_mtime_sec; 62 | private long st_mtime_nsec; 63 | private long st_ctime_sec; 64 | private long st_ctime_nsec; 65 | 66 | // created lazily 67 | private volatile String owner; 68 | private volatile String group; 69 | private volatile FileKey key; 70 | 71 | private FileAttributes(String filePath) { 72 | this.filePath = filePath; 73 | } 74 | 75 | private FileAttributes(FileDescriptor fileDescriptor) { 76 | this.fileDescriptor = fileDescriptor; 77 | } 78 | 79 | // get the FileAttributes for a given file 80 | public static FileAttributes get(String filePath, boolean followLinks) throws IOException { 81 | FileAttributes fileAttributes; 82 | 83 | if (filePath == null || filePath.isEmpty()) 84 | fileAttributes = new FileAttributes((String) null); 85 | else 86 | fileAttributes = new FileAttributes(new File(filePath).getAbsolutePath()); 87 | 88 | if (followLinks) { 89 | NativeDispatcher.stat(filePath, fileAttributes); 90 | } else { 91 | NativeDispatcher.lstat(filePath, fileAttributes); 92 | } 93 | 94 | // Logger.logDebug(fileAttributes.toString()); 95 | 96 | return fileAttributes; 97 | } 98 | 99 | // get the FileAttributes for an open file 100 | public static FileAttributes get(FileDescriptor fileDescriptor) throws IOException { 101 | FileAttributes fileAttributes = new FileAttributes(fileDescriptor); 102 | NativeDispatcher.fstat(fileDescriptor, fileAttributes); 103 | return fileAttributes; 104 | } 105 | 106 | public String file() { 107 | if (filePath != null) 108 | return filePath; 109 | else if (fileDescriptor != null) 110 | return fileDescriptor.toString(); 111 | else 112 | return null; 113 | } 114 | 115 | // package-private 116 | public boolean isSameFile(FileAttributes attrs) { 117 | return ((st_ino == attrs.st_ino) && (st_dev == attrs.st_dev)); 118 | } 119 | 120 | // package-private 121 | public int mode() { 122 | return st_mode; 123 | } 124 | 125 | public long blksize() { 126 | return st_blksize; 127 | } 128 | 129 | public long blocks() { 130 | return st_blocks; 131 | } 132 | 133 | public long ino() { 134 | return st_ino; 135 | } 136 | 137 | public long dev() { 138 | return st_dev; 139 | } 140 | 141 | public long rdev() { 142 | return st_rdev; 143 | } 144 | 145 | public long nlink() { 146 | return st_nlink; 147 | } 148 | 149 | public int uid() { 150 | return st_uid; 151 | } 152 | 153 | public int gid() { 154 | return st_gid; 155 | } 156 | 157 | private static FileTime toFileTime(long sec, long nsec) { 158 | if (nsec == 0) { 159 | return FileTime.from(sec, TimeUnit.SECONDS); 160 | } else { 161 | // truncate to microseconds to avoid overflow with timestamps 162 | // way out into the future. We can re-visit this if FileTime 163 | // is updated to define a from(secs,nsecs) method. 164 | long micro = sec * 1000000L + nsec / 1000L; 165 | return FileTime.from(micro, TimeUnit.MICROSECONDS); 166 | } 167 | } 168 | 169 | public FileTime lastAccessTime() { 170 | return toFileTime(st_atime_sec, st_atime_nsec); 171 | } 172 | 173 | public FileTime lastModifiedTime() { 174 | return toFileTime(st_mtime_sec, st_mtime_nsec); 175 | } 176 | 177 | public FileTime lastChangeTime() { 178 | return toFileTime(st_ctime_sec, st_ctime_nsec); 179 | } 180 | 181 | public FileTime creationTime() { 182 | return lastModifiedTime(); 183 | } 184 | 185 | public boolean isRegularFile() { 186 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFREG); 187 | } 188 | 189 | public boolean isDirectory() { 190 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFDIR); 191 | } 192 | 193 | public boolean isSymbolicLink() { 194 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFLNK); 195 | } 196 | 197 | public boolean isCharacter() { 198 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFCHR); 199 | } 200 | 201 | public boolean isFifo() { 202 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFIFO); 203 | } 204 | 205 | public boolean isSocket() { 206 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFSOCK); 207 | } 208 | 209 | public boolean isBlock() { 210 | return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK); 211 | } 212 | 213 | public boolean isOther() { 214 | int type = st_mode & UnixConstants.S_IFMT; 215 | return (type != UnixConstants.S_IFREG && 216 | type != UnixConstants.S_IFDIR && 217 | type != UnixConstants.S_IFLNK); 218 | } 219 | 220 | public boolean isDevice() { 221 | int type = st_mode & UnixConstants.S_IFMT; 222 | return (type == UnixConstants.S_IFCHR || 223 | type == UnixConstants.S_IFBLK || 224 | type == UnixConstants.S_IFIFO); 225 | } 226 | 227 | public long size() { 228 | return st_size; 229 | } 230 | 231 | public FileKey fileKey() { 232 | if (key == null) { 233 | synchronized (this) { 234 | if (key == null) { 235 | key = new FileKey(st_dev, st_ino); 236 | } 237 | } 238 | } 239 | return key; 240 | } 241 | 242 | public String owner() { 243 | if (owner == null) { 244 | synchronized (this) { 245 | if (owner == null) { 246 | owner = Integer.toString(st_uid); 247 | } 248 | } 249 | } 250 | return owner; 251 | } 252 | 253 | public String group() { 254 | if (group == null) { 255 | synchronized (this) { 256 | if (group == null) { 257 | group = Integer.toString(st_gid); 258 | } 259 | } 260 | } 261 | return group; 262 | } 263 | 264 | public Set permissions() { 265 | int bits = (st_mode & UnixConstants.S_IAMB); 266 | HashSet perms = new HashSet<>(); 267 | 268 | if ((bits & UnixConstants.S_IRUSR) > 0) 269 | perms.add(FilePermission.OWNER_READ); 270 | if ((bits & UnixConstants.S_IWUSR) > 0) 271 | perms.add(FilePermission.OWNER_WRITE); 272 | if ((bits & UnixConstants.S_IXUSR) > 0) 273 | perms.add(FilePermission.OWNER_EXECUTE); 274 | 275 | if ((bits & UnixConstants.S_IRGRP) > 0) 276 | perms.add(FilePermission.GROUP_READ); 277 | if ((bits & UnixConstants.S_IWGRP) > 0) 278 | perms.add(FilePermission.GROUP_WRITE); 279 | if ((bits & UnixConstants.S_IXGRP) > 0) 280 | perms.add(FilePermission.GROUP_EXECUTE); 281 | 282 | if ((bits & UnixConstants.S_IROTH) > 0) 283 | perms.add(FilePermission.OTHERS_READ); 284 | if ((bits & UnixConstants.S_IWOTH) > 0) 285 | perms.add(FilePermission.OTHERS_WRITE); 286 | if ((bits & UnixConstants.S_IXOTH) > 0) 287 | perms.add(FilePermission.OTHERS_EXECUTE); 288 | 289 | return perms; 290 | } 291 | 292 | public void loadFromStructStat(StructStat structStat) { 293 | this.st_mode = structStat.st_mode; 294 | this.st_ino = structStat.st_ino; 295 | this.st_dev = structStat.st_dev; 296 | this.st_rdev = structStat.st_rdev; 297 | this.st_nlink = structStat.st_nlink; 298 | this.st_uid = structStat.st_uid; 299 | this.st_gid = structStat.st_gid; 300 | this.st_size = structStat.st_size; 301 | this.st_blksize = structStat.st_blksize; 302 | this.st_blocks = structStat.st_blocks; 303 | 304 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { 305 | this.st_atime_sec = structStat.st_atim.tv_sec; 306 | this.st_atime_nsec = structStat.st_atim.tv_nsec; 307 | this.st_mtime_sec = structStat.st_mtim.tv_sec; 308 | this.st_mtime_nsec = structStat.st_mtim.tv_nsec; 309 | this.st_ctime_sec = structStat.st_ctim.tv_sec; 310 | this.st_ctime_nsec = structStat.st_ctim.tv_nsec; 311 | } else { 312 | this.st_atime_sec = structStat.st_atime; 313 | this.st_atime_nsec = 0; 314 | this.st_mtime_sec = structStat.st_mtime; 315 | this.st_mtime_nsec = 0; 316 | this.st_ctime_sec = structStat.st_ctime; 317 | this.st_ctime_nsec = 0; 318 | } 319 | } 320 | 321 | public String getFileString() { 322 | return "File: `" + file() + "`"; 323 | } 324 | 325 | public String getTypeString() { 326 | return "Type: `" + FileTypes.getFileType(this).getName() + "`"; 327 | } 328 | 329 | public String getSizeString() { 330 | return "Size: `" + size() + "`"; 331 | } 332 | 333 | public String getBlocksString() { 334 | return "Blocks: `" + blocks() + "`"; 335 | } 336 | 337 | public String getIOBlockString() { 338 | return "IO Block: `" + blksize() + "`"; 339 | } 340 | 341 | public String getDeviceString() { 342 | return "Device: `" + Long.toHexString(st_dev) + "`"; 343 | } 344 | 345 | public String getInodeString() { 346 | return "Inode: `" + st_ino + "`"; 347 | } 348 | 349 | public String getLinksString() { 350 | return "Links: `" + nlink() + "`"; 351 | } 352 | 353 | public String getDeviceTypeString() { 354 | return "Device Type: `" + rdev() + "`"; 355 | } 356 | 357 | public String getOwnerString() { 358 | return "Owner: `" + owner() + "`"; 359 | } 360 | 361 | public String getGroupString() { 362 | return "Group: `" + group() + "`"; 363 | } 364 | 365 | public String getPermissionString() { 366 | return "Permissions: `" + FilePermissions.toString(permissions()) + "`"; 367 | } 368 | 369 | public String getAccessTimeString() { 370 | return "Access Time: `" + lastAccessTime() + "`"; 371 | } 372 | 373 | public String getModifiedTimeString() { 374 | return "Modified Time: `" + lastModifiedTime() + "`"; 375 | } 376 | 377 | public String getChangeTimeString() { 378 | return "Change Time: `" + lastChangeTime() + "`"; 379 | } 380 | 381 | @NonNull 382 | @Override 383 | public String toString() { 384 | return getFileAttributesLogString(this); 385 | } 386 | 387 | public static String getFileAttributesLogString(final FileAttributes fileAttributes) { 388 | if (fileAttributes == null) return "null"; 389 | 390 | StringBuilder logString = new StringBuilder(); 391 | 392 | logString.append(fileAttributes.getFileString()); 393 | 394 | logString.append("\n").append(fileAttributes.getTypeString()); 395 | 396 | logString.append("\n").append(fileAttributes.getSizeString()); 397 | logString.append("\n").append(fileAttributes.getBlocksString()); 398 | logString.append("\n").append(fileAttributes.getIOBlockString()); 399 | 400 | logString.append("\n").append(fileAttributes.getDeviceString()); 401 | logString.append("\n").append(fileAttributes.getInodeString()); 402 | logString.append("\n").append(fileAttributes.getLinksString()); 403 | 404 | if (fileAttributes.isBlock() || fileAttributes.isCharacter()) 405 | logString.append("\n").append(fileAttributes.getDeviceTypeString()); 406 | 407 | logString.append("\n").append(fileAttributes.getOwnerString()); 408 | logString.append("\n").append(fileAttributes.getGroupString()); 409 | logString.append("\n").append(fileAttributes.getPermissionString()); 410 | 411 | logString.append("\n").append(fileAttributes.getAccessTimeString()); 412 | logString.append("\n").append(fileAttributes.getModifiedTimeString()); 413 | logString.append("\n").append(fileAttributes.getChangeTimeString()); 414 | 415 | return logString.toString(); 416 | } 417 | 418 | } 419 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FileKey.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Oracle designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Oracle in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 | * or visit www.oracle.com if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | package net.dinglisch.android.appfactory.utils.file.filesystem; 27 | 28 | /** 29 | * Container for device/inode to uniquely identify file. 30 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileKey.java 31 | */ 32 | 33 | public class FileKey { 34 | private final long st_dev; 35 | private final long st_ino; 36 | 37 | FileKey(long st_dev, long st_ino) { 38 | this.st_dev = st_dev; 39 | this.st_ino = st_ino; 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return (int)(st_dev ^ (st_dev >>> 32)) + 45 | (int)(st_ino ^ (st_ino >>> 32)); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object obj) { 50 | if (obj == this) 51 | return true; 52 | if (!(obj instanceof FileKey)) 53 | return false; 54 | FileKey other = (FileKey)obj; 55 | return (this.st_dev == other.st_dev) && (this.st_ino == other.st_ino); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | StringBuilder sb = new StringBuilder(); 61 | sb.append("(dev=") 62 | .append(Long.toHexString(st_dev)) 63 | .append(",ino=") 64 | .append(st_ino) 65 | .append(')'); 66 | return sb.toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FilePermission.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved. 4 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 | * 6 | * This code is free software; you can redistribute it and/or modify it 7 | * under the terms of the GNU General Public License version 2 only, as 8 | * published by the Free Software Foundation. Oracle designates this 9 | * particular file as subject to the "Classpath" exception as provided 10 | * by Oracle in the LICENSE file that accompanied this code. 11 | * 12 | * This code is distributed in the hope that it will be useful, but WITHOUT 13 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 | * version 2 for more details (a copy is included in the LICENSE file that 16 | * accompanied this code). 17 | * 18 | * You should have received a copy of the GNU General Public License version 19 | * 2 along with this work; if not, write to the Free Software Foundation, 20 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | * 22 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 23 | * or visit www.oracle.com if you need additional information or have any 24 | * questions. 25 | */ 26 | 27 | package net.dinglisch.android.appfactory.utils.file.filesystem; 28 | 29 | /** 30 | * Defines the bits for use with the {@link FileAttributes#permissions() 31 | * permissions} attribute. 32 | * 33 | *

The {@link FileAttributes} class defines methods for manipulating 34 | * set of permissions. 35 | * 36 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermission.java 37 | * 38 | * @since 1.7 39 | */ 40 | 41 | public enum FilePermission { 42 | 43 | /** 44 | * Read permission, owner. 45 | */ 46 | OWNER_READ, 47 | 48 | /** 49 | * Write permission, owner. 50 | */ 51 | OWNER_WRITE, 52 | 53 | /** 54 | * Execute/search permission, owner. 55 | */ 56 | OWNER_EXECUTE, 57 | 58 | /** 59 | * Read permission, group. 60 | */ 61 | GROUP_READ, 62 | 63 | /** 64 | * Write permission, group. 65 | */ 66 | GROUP_WRITE, 67 | 68 | /** 69 | * Execute/search permission, group. 70 | */ 71 | GROUP_EXECUTE, 72 | 73 | /** 74 | * Read permission, others. 75 | */ 76 | OTHERS_READ, 77 | 78 | /** 79 | * Write permission, others. 80 | */ 81 | OTHERS_WRITE, 82 | 83 | /** 84 | * Execute/search permission, others. 85 | */ 86 | OTHERS_EXECUTE 87 | 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FilePermissions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Oracle designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Oracle in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 | * or visit www.oracle.com if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | package net.dinglisch.android.appfactory.utils.file.filesystem; 27 | 28 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.GROUP_EXECUTE; 29 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.GROUP_READ; 30 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.GROUP_WRITE; 31 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.OTHERS_EXECUTE; 32 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.OTHERS_READ; 33 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.OTHERS_WRITE; 34 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.OWNER_EXECUTE; 35 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.OWNER_READ; 36 | import static net.dinglisch.android.appfactory.utils.file.filesystem.FilePermission.OWNER_WRITE; 37 | 38 | import java.util.EnumSet; 39 | import java.util.Set; 40 | 41 | /** 42 | * This class consists exclusively of static methods that operate on sets of 43 | * {@link FilePermission} objects. 44 | * 45 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermissions.java 46 | * 47 | * @since 1.7 48 | */ 49 | 50 | public final class FilePermissions { 51 | private FilePermissions() { } 52 | 53 | // Write string representation of permission bits to {@code sb}. 54 | private static void writeBits(StringBuilder sb, boolean r, boolean w, boolean x) { 55 | if (r) { 56 | sb.append('r'); 57 | } else { 58 | sb.append('-'); 59 | } 60 | if (w) { 61 | sb.append('w'); 62 | } else { 63 | sb.append('-'); 64 | } 65 | if (x) { 66 | sb.append('x'); 67 | } else { 68 | sb.append('-'); 69 | } 70 | } 71 | 72 | /** 73 | * Returns the {@code String} representation of a set of permissions. It 74 | * is guaranteed that the returned {@code String} can be parsed by the 75 | * {@link #fromString} method. 76 | * 77 | *

If the set contains {@code null} or elements that are not of type 78 | * {@code FilePermission} then these elements are ignored. 79 | * 80 | * @param perms 81 | * the set of permissions 82 | * 83 | * @return the string representation of the permission set 84 | */ 85 | public static String toString(Set perms) { 86 | StringBuilder sb = new StringBuilder(9); 87 | writeBits(sb, perms.contains(OWNER_READ), perms.contains(OWNER_WRITE), 88 | perms.contains(OWNER_EXECUTE)); 89 | writeBits(sb, perms.contains(GROUP_READ), perms.contains(GROUP_WRITE), 90 | perms.contains(GROUP_EXECUTE)); 91 | writeBits(sb, perms.contains(OTHERS_READ), perms.contains(OTHERS_WRITE), 92 | perms.contains(OTHERS_EXECUTE)); 93 | return sb.toString(); 94 | } 95 | 96 | private static boolean isSet(char c, char setValue) { 97 | if (c == setValue) 98 | return true; 99 | if (c == '-') 100 | return false; 101 | throw new IllegalArgumentException("Invalid mode"); 102 | } 103 | private static boolean isR(char c) { return isSet(c, 'r'); } 104 | private static boolean isW(char c) { return isSet(c, 'w'); } 105 | private static boolean isX(char c) { return isSet(c, 'x'); } 106 | 107 | /** 108 | * Returns the set of permissions corresponding to a given {@code String} 109 | * representation. 110 | * 111 | *

The {@code perms} parameter is a {@code String} representing the 112 | * permissions. It has 9 characters that are interpreted as three sets of 113 | * three. The first set refers to the owner's permissions; the next to the 114 | * group permissions and the last to others. Within each set, the first 115 | * character is {@code 'r'} to indicate permission to read, the second 116 | * character is {@code 'w'} to indicate permission to write, and the third 117 | * character is {@code 'x'} for execute permission. Where a permission is 118 | * not set then the corresponding character is set to {@code '-'}. 119 | * 120 | *

Usage Example: 121 | * Suppose we require the set of permissions that indicate the owner has read, 122 | * write, and execute permissions, the group has read and execute permissions 123 | * and others have none. 124 | *

125 |      *   Set<FilePermission> perms = FilePermissions.fromString("rwxr-x---");
126 |      * 
127 | * 128 | * @param perms 129 | * string representing a set of permissions 130 | * 131 | * @return the resulting set of permissions 132 | * 133 | * @throws IllegalArgumentException 134 | * if the string cannot be converted to a set of permissions 135 | * 136 | * @see #toString(Set) 137 | */ 138 | public static Set fromString(String perms) { 139 | if (perms.length() != 9) 140 | throw new IllegalArgumentException("Invalid mode"); 141 | Set result = EnumSet.noneOf(FilePermission.class); 142 | if (isR(perms.charAt(0))) result.add(OWNER_READ); 143 | if (isW(perms.charAt(1))) result.add(OWNER_WRITE); 144 | if (isX(perms.charAt(2))) result.add(OWNER_EXECUTE); 145 | if (isR(perms.charAt(3))) result.add(GROUP_READ); 146 | if (isW(perms.charAt(4))) result.add(GROUP_WRITE); 147 | if (isX(perms.charAt(5))) result.add(GROUP_EXECUTE); 148 | if (isR(perms.charAt(6))) result.add(OTHERS_READ); 149 | if (isW(perms.charAt(7))) result.add(OTHERS_WRITE); 150 | if (isX(perms.charAt(8))) result.add(OTHERS_EXECUTE); 151 | return result; 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FileTime.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved. 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 | * 5 | * This code is free software; you can redistribute it and/or modify it 6 | * under the terms of the GNU General Public License version 2 only, as 7 | * published by the Free Software Foundation. Oracle designates this 8 | * particular file as subject to the "Classpath" exception as provided 9 | * by Oracle in the LICENSE file that accompanied this code. 10 | * 11 | * This code is distributed in the hope that it will be useful, but WITHOUT 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | * version 2 for more details (a copy is included in the LICENSE file that 15 | * accompanied this code). 16 | * 17 | * You should have received a copy of the GNU General Public License version 18 | * 2 along with this work; if not, write to the Free Software Foundation, 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 | * or visit www.oracle.com if you need additional information or have any 23 | * questions. 24 | */ 25 | 26 | package net.dinglisch.android.appfactory.utils.file.filesystem; 27 | 28 | import androidx.annotation.NonNull; 29 | 30 | import java.text.SimpleDateFormat; 31 | import java.util.Calendar; 32 | import java.util.Objects; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | /** 36 | * Represents the value of a file's time stamp attribute. For example, it may 37 | * represent the time that the file was last 38 | * {@link FileAttributes#lastModifiedTime() modified}, 39 | * {@link FileAttributes#lastAccessTime() accessed}, 40 | * or {@link FileAttributes#creationTime() created}. 41 | * 42 | *

Instances of this class are immutable. 43 | * 44 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/FileTime.java 45 | * 46 | * @since 1.7 47 | * @see java.nio.file.Files#setLastModifiedTime 48 | * @see java.nio.file.Files#getLastModifiedTime 49 | */ 50 | 51 | public final class FileTime { 52 | /** 53 | * The unit of granularity to interpret the value. Null if 54 | * this {@code FileTime} is converted from an {@code Instant}, 55 | * the {@code value} and {@code unit} pair will not be used 56 | * in this scenario. 57 | */ 58 | private final TimeUnit unit; 59 | 60 | /** 61 | * The value since the epoch; can be negative. 62 | */ 63 | private final long value; 64 | 65 | 66 | /** 67 | * The value return by toString (created lazily) 68 | */ 69 | private String valueAsString; 70 | 71 | /** 72 | * Initializes a new instance of this class. 73 | */ 74 | private FileTime(long value, TimeUnit unit) { 75 | this.value = value; 76 | this.unit = unit; 77 | } 78 | 79 | /** 80 | * Returns a {@code FileTime} representing a value at the given unit of 81 | * granularity. 82 | * 83 | * @param value 84 | * the value since the epoch (1970-01-01T00:00:00Z); can be 85 | * negative 86 | * @param unit 87 | * the unit of granularity to interpret the value 88 | * 89 | * @return a {@code FileTime} representing the given value 90 | */ 91 | public static FileTime from(long value, @NonNull TimeUnit unit) { 92 | Objects.requireNonNull(unit, "unit"); 93 | return new FileTime(value, unit); 94 | } 95 | 96 | /** 97 | * Returns a {@code FileTime} representing the given value in milliseconds. 98 | * 99 | * @param value 100 | * the value, in milliseconds, since the epoch 101 | * (1970-01-01T00:00:00Z); can be negative 102 | * 103 | * @return a {@code FileTime} representing the given value 104 | */ 105 | public static FileTime fromMillis(long value) { 106 | return new FileTime(value, TimeUnit.MILLISECONDS); 107 | } 108 | 109 | /** 110 | * Returns the value at the given unit of granularity. 111 | * 112 | *

Conversion from a coarser granularity that would numerically overflow 113 | * saturate to {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE} 114 | * if positive. 115 | * 116 | * @param unit 117 | * the unit of granularity for the return value 118 | * 119 | * @return value in the given unit of granularity, since the epoch 120 | * since the epoch (1970-01-01T00:00:00Z); can be negative 121 | */ 122 | public long to(TimeUnit unit) { 123 | Objects.requireNonNull(unit, "unit"); 124 | return unit.convert(this.value, this.unit); 125 | } 126 | 127 | /** 128 | * Returns the value in milliseconds. 129 | * 130 | *

Conversion from a coarser granularity that would numerically overflow 131 | * saturate to {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE} 132 | * if positive. 133 | * 134 | * @return the value in milliseconds, since the epoch (1970-01-01T00:00:00Z) 135 | */ 136 | public long toMillis() { 137 | return unit.toMillis(value); 138 | } 139 | 140 | @NonNull 141 | @Override 142 | public String toString() { 143 | return getDate(toMillis(), "yyyy.MM.dd HH:mm:ss.SSS z"); 144 | } 145 | 146 | public static String getDate(long milliSeconds, String format) { 147 | try { 148 | Calendar calendar = Calendar.getInstance(); 149 | calendar.setTimeInMillis(milliSeconds); 150 | return new SimpleDateFormat(format).format(calendar.getTime()); 151 | } catch(Exception e) { 152 | return Long.toString(milliSeconds); 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FileType.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.file.filesystem; 2 | 3 | /** The {@link Enum} that defines file types. */ 4 | public enum FileType { 5 | 6 | NO_EXIST("no exist", 0), // 00000000 7 | REGULAR("regular", 1), // 00000001 8 | DIRECTORY("directory", 2), // 00000010 9 | SYMLINK("symlink", 4), // 00000100 10 | SOCKET("socket", 8), // 00001000 11 | CHARACTER("character", 16), // 00010000 12 | FIFO("fifo", 32), // 00100000 13 | BLOCK("block", 64), // 01000000 14 | UNKNOWN("unknown", 128); // 10000000 15 | 16 | private final String name; 17 | private final int value; 18 | 19 | FileType(final String name, final int value) { 20 | this.name = name; 21 | this.value = value; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public int getValue() { 29 | return value; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/FileTypes.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.file.filesystem; 2 | 3 | import android.system.Os; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import net.dinglisch.android.appfactory.utils.logger.Logger; 8 | 9 | import java.io.File; 10 | 11 | public class FileTypes { 12 | 13 | /** Flags to represent regular, directory and symlink file types defined by {@link FileType} */ 14 | public static final int FILE_TYPE_NORMAL_FLAGS = FileType.REGULAR.getValue() | FileType.DIRECTORY.getValue() | FileType.SYMLINK.getValue(); 15 | 16 | /** Flags to represent any file type defined by {@link FileType} */ 17 | public static final int FILE_TYPE_ANY_FLAGS = Integer.MAX_VALUE; // 1111111111111111111111111111111 (31 1's) 18 | 19 | public static String convertFileTypeFlagsToNamesString(int fileTypeFlags) { 20 | StringBuilder fileTypeFlagsStringBuilder = new StringBuilder(); 21 | 22 | FileType[] fileTypes = {FileType.REGULAR, FileType.DIRECTORY, FileType.SYMLINK, FileType.CHARACTER, FileType.FIFO, FileType.BLOCK, FileType.UNKNOWN}; 23 | for (FileType fileType : fileTypes) { 24 | if ((fileTypeFlags & fileType.getValue()) > 0) 25 | fileTypeFlagsStringBuilder.append(fileType.getName()).append(","); 26 | } 27 | 28 | String fileTypeFlagsString = fileTypeFlagsStringBuilder.toString(); 29 | 30 | if (fileTypeFlagsString.endsWith(",")) 31 | fileTypeFlagsString = fileTypeFlagsString.substring(0, fileTypeFlagsString.lastIndexOf(",")); 32 | 33 | return fileTypeFlagsString; 34 | } 35 | 36 | /** 37 | * Checks the type of file that exists at {@code filePath}. 38 | * 39 | * Returns: 40 | * - {@link FileType#NO_EXIST} if {@code filePath} is {@code null}, empty, an exception is raised 41 | * or no file exists at {@code filePath}. 42 | * - {@link FileType#REGULAR} if file at {@code filePath} is a regular file. 43 | * - {@link FileType#DIRECTORY} if file at {@code filePath} is a directory file. 44 | * - {@link FileType#SYMLINK} if file at {@code filePath} is a symlink file and {@code followLinks} is {@code false}. 45 | * - {@link FileType#CHARACTER} if file at {@code filePath} is a character special file. 46 | * - {@link FileType#FIFO} if file at {@code filePath} is a fifo special file. 47 | * - {@link FileType#BLOCK} if file at {@code filePath} is a block special file. 48 | * - {@link FileType#UNKNOWN} if file at {@code filePath} is of unknown type. 49 | * 50 | * The {@link File#isFile()} and {@link File#isDirectory()} uses {@link Os#stat(String)} system 51 | * call (not {@link Os#lstat(String)}) to check file type and does follow symlinks. 52 | * 53 | * The {@link File#exists()} uses {@link Os#access(String, int)} system call to check if file is 54 | * accessible and does not follow symlinks. However, it returns {@code false} for dangling symlinks, 55 | * on android at least. Check https://stackoverflow.com/a/57747064/14686958 56 | * 57 | * Basically {@link File} API is not reliable to check for symlinks. 58 | * 59 | * So we get the file type directly with {@link Os#lstat(String)} if {@code followLinks} is 60 | * {@code false} and {@link Os#stat(String)} if {@code followLinks} is {@code true}. All exceptions 61 | * are assumed as non-existence. 62 | * 63 | * The {@link org.apache.commons.io.FileUtils#isSymlink(File)} can also be used for checking 64 | * symlinks but {@link FileAttributes} will provide access to more attributes if necessary, 65 | * including getting other special file types considering that {@link File#exists()} can't be 66 | * used to reliably check for non-existence and exclude the other 3 file types. commons.io is 67 | * also not compatible with android < 8 for many things. 68 | * 69 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/File.java;l=793 70 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/UnixFileSystem.java;l=248 71 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/native/UnixFileSystem_md.c;l=121 72 | * https://cs.android.com/android/_/android/platform/libcore/+/001ac51d61ad7443ba518bf2cf7e086efe698c6d 73 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/libcore/io/Os.java;l=51 74 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/libcore/io/Libcore.java;l=45 75 | * https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7530 76 | * 77 | * @param filePath The {@code path} for file to check. 78 | * @param followLinks The {@code boolean} that decides if symlinks will be followed while 79 | * finding type. If set to {@code true}, then type of symlink target will 80 | * be returned if file at {@code filePath} is a symlink. If set to 81 | * {@code false}, then type of file at {@code filePath} itself will be 82 | * returned. 83 | * @return Returns the {@link FileType} of file. 84 | */ 85 | @NonNull 86 | public static FileType getFileType(final String filePath, final boolean followLinks) { 87 | if (filePath == null || filePath.isEmpty()) return FileType.NO_EXIST; 88 | 89 | try { 90 | FileAttributes fileAttributes = FileAttributes.get(filePath, followLinks); 91 | return getFileType(fileAttributes); 92 | } catch (Exception e) { 93 | // If not a ENOENT (No such file or directory) exception 94 | if (e.getMessage() != null && !e.getMessage().contains("ENOENT")) 95 | Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage()); 96 | return FileType.NO_EXIST; 97 | } 98 | } 99 | 100 | public static FileType getFileType(@NonNull final FileAttributes fileAttributes) { 101 | if (fileAttributes.isRegularFile()) 102 | return FileType.REGULAR; 103 | else if (fileAttributes.isDirectory()) 104 | return FileType.DIRECTORY; 105 | else if (fileAttributes.isSymbolicLink()) 106 | return FileType.SYMLINK; 107 | else if (fileAttributes.isSocket()) 108 | return FileType.SOCKET; 109 | else if (fileAttributes.isCharacter()) 110 | return FileType.CHARACTER; 111 | else if (fileAttributes.isFifo()) 112 | return FileType.FIFO; 113 | else if (fileAttributes.isBlock()) 114 | return FileType.BLOCK; 115 | else 116 | return FileType.UNKNOWN; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/NativeDispatcher.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.file.filesystem; 2 | 3 | import android.system.ErrnoException; 4 | import android.system.Os; 5 | 6 | import java.io.File; 7 | import java.io.FileDescriptor; 8 | import java.io.IOException; 9 | 10 | public class NativeDispatcher { 11 | 12 | public static void stat(String filePath, FileAttributes fileAttributes) throws IOException { 13 | validateFileExistence(filePath); 14 | 15 | try { 16 | fileAttributes.loadFromStructStat(Os.stat(filePath)); 17 | } catch (ErrnoException e) { 18 | throw new IOException("Failed to run Os.stat() on file at path \"" + filePath + "\": " + e.getMessage()); 19 | } 20 | } 21 | 22 | public static void lstat(String filePath, FileAttributes fileAttributes) throws IOException { 23 | validateFileExistence(filePath); 24 | 25 | try { 26 | fileAttributes.loadFromStructStat(Os.lstat(filePath)); 27 | } catch (ErrnoException e) { 28 | throw new IOException("Failed to run Os.lstat() on file at path \"" + filePath + "\": " + e.getMessage()); 29 | } 30 | } 31 | 32 | public static void fstat(FileDescriptor fileDescriptor, FileAttributes fileAttributes) throws IOException { 33 | validateFileDescriptor(fileDescriptor); 34 | 35 | try { 36 | fileAttributes.loadFromStructStat(Os.fstat(fileDescriptor)); 37 | } catch (ErrnoException e) { 38 | throw new IOException("Failed to run Os.fstat() on file descriptor \"" + fileDescriptor.toString() + "\": " + e.getMessage()); 39 | } 40 | } 41 | 42 | public static void validateFileExistence(String filePath) throws IOException { 43 | if (filePath == null || filePath.isEmpty()) throw new IOException("The path is null or empty"); 44 | 45 | File file = new File(filePath); 46 | 47 | //if (!file.exists()) 48 | // throw new IOException("No such file or directory: \"" + filePath + "\""); 49 | } 50 | 51 | public static void validateFileDescriptor(FileDescriptor fileDescriptor) throws IOException { 52 | if (fileDescriptor == null) throw new IOException("The file descriptor is null"); 53 | 54 | if (!fileDescriptor.valid()) 55 | throw new IOException("No such file descriptor: \"" + fileDescriptor.toString() + "\""); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/file/filesystem/UnixConstants.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved. 4 | * 5 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 | * 7 | * This code is free software; you can redistribute it and/or modify it 8 | * under the terms of the GNU General Public License version 2 only, as 9 | * published by the Free Software Foundation. Oracle designates this 10 | * particular file as subject to the "Classpath" exception as provided 11 | * by Oracle in the LICENSE file that accompanied this code. 12 | * 13 | * This code is distributed in the hope that it will be useful, but WITHOUT 14 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 | * version 2 for more details (a copy is included in the LICENSE file that 17 | * accompanied this code). 18 | * 19 | * You should have received a copy of the GNU General Public License version 20 | * 2 along with this work; if not, write to the Free Software Foundation, 21 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 | * 23 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 | * or visit www.oracle.com if you need additional information or have any 25 | * questions. 26 | * 27 | */ 28 | // AUTOMATICALLY GENERATED FILE - DO NOT EDIT 29 | package net.dinglisch.android.appfactory.utils.file.filesystem; 30 | 31 | // BEGIN Android-changed: Use constants from android.system.OsConstants. http://b/32203242 32 | // Those constants are initialized by native code to ensure correctness on different architectures. 33 | // AT_SYMLINK_NOFOLLOW (used by fstatat) and AT_REMOVEDIR (used by unlinkat) as of July 2018 do not 34 | // have equivalents in android.system.OsConstants so left unchanged. 35 | import android.system.OsConstants; 36 | 37 | /** 38 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixConstants.java 39 | */ 40 | public class UnixConstants { 41 | private UnixConstants() { } 42 | 43 | static final int O_RDONLY = OsConstants.O_RDONLY; 44 | 45 | static final int O_WRONLY = OsConstants.O_WRONLY; 46 | 47 | static final int O_RDWR = OsConstants.O_RDWR; 48 | 49 | static final int O_APPEND = OsConstants.O_APPEND; 50 | 51 | static final int O_CREAT = OsConstants.O_CREAT; 52 | 53 | static final int O_EXCL = OsConstants.O_EXCL; 54 | 55 | static final int O_TRUNC = OsConstants.O_TRUNC; 56 | 57 | static final int O_SYNC = OsConstants.O_SYNC; 58 | 59 | // Crash on Android 5. 60 | // No static field O_DSYNC of type I in class Landroid/system/OsConstants; or its superclasses 61 | // (declaration of 'android.system.OsConstants' appears in /system/framework/core-libart.jar) 62 | //@RequiresApi(Build.VERSION_CODES.O_MR1) 63 | //static final int O_DSYNC = OsConstants.O_DSYNC; 64 | 65 | static final int O_NOFOLLOW = OsConstants.O_NOFOLLOW; 66 | 67 | static final int S_IAMB = get_S_IAMB(); 68 | 69 | static final int S_IRUSR = OsConstants.S_IRUSR; 70 | 71 | static final int S_IWUSR = OsConstants.S_IWUSR; 72 | 73 | static final int S_IXUSR = OsConstants.S_IXUSR; 74 | 75 | static final int S_IRGRP = OsConstants.S_IRGRP; 76 | 77 | static final int S_IWGRP = OsConstants.S_IWGRP; 78 | 79 | static final int S_IXGRP = OsConstants.S_IXGRP; 80 | 81 | static final int S_IROTH = OsConstants.S_IROTH; 82 | 83 | static final int S_IWOTH = OsConstants.S_IWOTH; 84 | 85 | static final int S_IXOTH = OsConstants.S_IXOTH; 86 | 87 | static final int S_IFMT = OsConstants.S_IFMT; 88 | 89 | static final int S_IFREG = OsConstants.S_IFREG; 90 | 91 | static final int S_IFDIR = OsConstants.S_IFDIR; 92 | 93 | static final int S_IFLNK = OsConstants.S_IFLNK; 94 | 95 | static final int S_IFSOCK = OsConstants.S_IFSOCK; 96 | 97 | static final int S_IFCHR = OsConstants.S_IFCHR; 98 | 99 | static final int S_IFBLK = OsConstants.S_IFBLK; 100 | 101 | static final int S_IFIFO = OsConstants.S_IFIFO; 102 | 103 | static final int R_OK = OsConstants.R_OK; 104 | 105 | static final int W_OK = OsConstants.W_OK; 106 | 107 | static final int X_OK = OsConstants.X_OK; 108 | 109 | static final int F_OK = OsConstants.F_OK; 110 | 111 | static final int ENOENT = OsConstants.ENOENT; 112 | 113 | static final int EACCES = OsConstants.EACCES; 114 | 115 | static final int EEXIST = OsConstants.EEXIST; 116 | 117 | static final int ENOTDIR = OsConstants.ENOTDIR; 118 | 119 | static final int EINVAL = OsConstants.EINVAL; 120 | 121 | static final int EXDEV = OsConstants.EXDEV; 122 | 123 | static final int EISDIR = OsConstants.EISDIR; 124 | 125 | static final int ENOTEMPTY = OsConstants.ENOTEMPTY; 126 | 127 | static final int ENOSPC = OsConstants.ENOSPC; 128 | 129 | static final int EAGAIN = OsConstants.EAGAIN; 130 | 131 | static final int ENOSYS = OsConstants.ENOSYS; 132 | 133 | static final int ELOOP = OsConstants.ELOOP; 134 | 135 | static final int EROFS = OsConstants.EROFS; 136 | 137 | static final int ENODATA = OsConstants.ENODATA; 138 | 139 | static final int ERANGE = OsConstants.ERANGE; 140 | 141 | static final int EMFILE = OsConstants.EMFILE; 142 | 143 | // S_IAMB are access mode bits, therefore, calculated by taking OR of all the read, write and 144 | // execute permissions bits for owner, group and other. 145 | private static int get_S_IAMB() { 146 | return (OsConstants.S_IRUSR | OsConstants.S_IWUSR | OsConstants.S_IXUSR | 147 | OsConstants.S_IRGRP | OsConstants.S_IWGRP | OsConstants.S_IXGRP | 148 | OsConstants.S_IROTH | OsConstants.S_IWOTH | OsConstants.S_IXOTH); 149 | } 150 | // END Android-changed: Use constants from android.system.OsConstants. http://b/32203242 151 | 152 | 153 | static final int AT_SYMLINK_NOFOLLOW = 0x100; 154 | static final int AT_REMOVEDIR = 0x200; 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/markdown/MarkdownUtils.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.markdown; 2 | 3 | import com.google.common.base.Strings; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class MarkdownUtils { 9 | 10 | public static final String backtick = "`"; 11 | public static final Pattern backticksPattern = Pattern.compile("(" + backtick + "+)"); 12 | 13 | /** 14 | * Get the markdown code {@link String} for a {@link String}. This ensures all backticks "`" are 15 | * properly escaped so that markdown does not break. 16 | * 17 | * @param string The {@link String} to convert. 18 | * @param codeBlock If the {@link String} is to be converted to a code block or inline code. 19 | * @return Returns the markdown code {@link String}. 20 | */ 21 | public static String getMarkdownCodeForString(String string, boolean codeBlock) { 22 | if (string == null) return null; 23 | if (string.isEmpty()) return ""; 24 | 25 | int maxConsecutiveBackTicksCount = getMaxConsecutiveBackTicksCount(string); 26 | 27 | // markdown requires surrounding backticks count to be at least one more than the count 28 | // of consecutive ticks in the string itself 29 | int backticksCountToUse; 30 | if (codeBlock) 31 | backticksCountToUse = maxConsecutiveBackTicksCount + 3; 32 | else 33 | backticksCountToUse = maxConsecutiveBackTicksCount + 1; 34 | 35 | // create a string with n backticks where n==backticksCountToUse 36 | String backticksToUse = Strings.repeat(backtick, backticksCountToUse); 37 | 38 | if (codeBlock) 39 | return backticksToUse + "\n" + string + "\n" + backticksToUse; 40 | else { 41 | // add a space to any prefixed or suffixed backtick characters 42 | if (string.startsWith(backtick)) 43 | string = " " + string; 44 | if (string.endsWith(backtick)) 45 | string = string + " "; 46 | 47 | return backticksToUse + string + backticksToUse; 48 | } 49 | } 50 | 51 | /** 52 | * Get the max consecutive backticks "`" in a {@link String}. 53 | * 54 | * @param string The {@link String} to check. 55 | * @return Returns the max consecutive backticks count. 56 | */ 57 | public static int getMaxConsecutiveBackTicksCount(String string) { 58 | if (string == null || string.isEmpty()) return 0; 59 | 60 | int maxCount = 0; 61 | int matchCount; 62 | String match; 63 | 64 | Matcher matcher = backticksPattern.matcher(string); 65 | while(matcher.find()) { 66 | match = matcher.group(1); 67 | matchCount = match != null ? match.length() : 0; 68 | if (matchCount > maxCount) 69 | maxCount = matchCount; 70 | } 71 | 72 | return maxCount; 73 | } 74 | 75 | 76 | 77 | public static String getLiteralSingleLineMarkdownStringEntry(String label, Object object, String def) { 78 | return "**" + label + "**: " + (object != null ? object.toString() : def) + " "; 79 | } 80 | 81 | public static String getSingleLineMarkdownStringEntry(String label, Object object, String def) { 82 | if (object != null) 83 | return "**" + label + "**: " + getMarkdownCodeForString(object.toString(), false) + " "; 84 | else 85 | return "**" + label + "**: " + def + " "; 86 | } 87 | 88 | public static String getMultiLineMarkdownStringEntry(String label, Object object, String def) { 89 | if (object != null) 90 | return "**" + label + "**:\n" + getMarkdownCodeForString(object.toString(), true) + "\n"; 91 | else 92 | return "**" + label + "**: " + def + "\n"; 93 | } 94 | 95 | public static String getLinkMarkdownString(String label, String url) { 96 | if (url != null) 97 | return "[" + label.replaceAll("]", "\\\\]") + "](" + url.replaceAll("\\)", "\\\\)") + ")"; 98 | else 99 | return label; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/ShellUtils.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import net.dinglisch.android.appfactory.utils.file.FileUtils; 7 | 8 | import java.lang.reflect.Field; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class ShellUtils { 14 | 15 | /** Get process id of {@link Process}. */ 16 | public static int getPid(Process p) { 17 | try { 18 | Field f = p.getClass().getDeclaredField("pid"); 19 | f.setAccessible(true); 20 | try { 21 | return f.getInt(p); 22 | } finally { 23 | f.setAccessible(false); 24 | } 25 | } catch (Throwable e) { 26 | return -1; 27 | } 28 | } 29 | 30 | /** Setup shell command arguments for the execute. */ 31 | @NonNull 32 | public static String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) { 33 | List result = new ArrayList<>(); 34 | result.add(executable); 35 | if (arguments != null) Collections.addAll(result, arguments); 36 | return result.toArray(new String[0]); 37 | } 38 | 39 | /** Get basename for executable. */ 40 | @Nullable 41 | public static String getExecutableBasename(@Nullable String executable) { 42 | return FileUtils.getFileBasename(executable); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/StreamGobbler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.dinglisch.android.appfactory.utils.shell; 18 | 19 | import androidx.annotation.AnyThread; 20 | import androidx.annotation.NonNull; 21 | import androidx.annotation.Nullable; 22 | import androidx.annotation.WorkerThread; 23 | 24 | import net.dinglisch.android.appfactory.utils.logger.Logger; 25 | 26 | import java.io.BufferedReader; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.InputStreamReader; 30 | import java.util.List; 31 | import java.util.Locale; 32 | 33 | /** 34 | * Thread utility class continuously reading from an InputStream 35 | * 36 | * https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java#L141 37 | * https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java 38 | */ 39 | @SuppressWarnings({"WeakerAccess"}) 40 | public class StreamGobbler extends Thread { 41 | private static int threadCounter = 0; 42 | private static int incThreadCounter() { 43 | synchronized (StreamGobbler.class) { 44 | int ret = threadCounter; 45 | threadCounter++; 46 | return ret; 47 | } 48 | } 49 | 50 | /** 51 | * Line callback interface 52 | */ 53 | public interface OnLineListener { 54 | /** 55 | *

Line callback

56 | * 57 | *

This callback should process the line as quickly as possible. 58 | * Delays in this callback may pause the native process or even 59 | * result in a deadlock

60 | * 61 | * @param line String that was gobbled 62 | */ 63 | void onLine(String line); 64 | } 65 | 66 | /** 67 | * Stream closed callback interface 68 | */ 69 | public interface OnStreamClosedListener { 70 | /** 71 | *

Stream closed callback

72 | */ 73 | void onStreamClosed(); 74 | } 75 | 76 | @NonNull 77 | private final String shell; 78 | @NonNull 79 | private final InputStream inputStream; 80 | @NonNull 81 | private final BufferedReader reader; 82 | @Nullable 83 | private final List listWriter; 84 | @Nullable 85 | private final StringBuilder stringWriter; 86 | @Nullable 87 | private final OnLineListener lineListener; 88 | @Nullable 89 | private final OnStreamClosedListener streamClosedListener; 90 | @Nullable 91 | private final Integer mLogLevel; 92 | private volatile boolean active = true; 93 | private volatile boolean calledOnClose = false; 94 | 95 | private static final String LOG_TAG = "StreamGobbler"; 96 | 97 | /** 98 | *

StreamGobbler constructor

99 | * 100 | *

We use this class because shell STDOUT and STDERR should be read as quickly as 101 | * possible to prevent a deadlock from occurring, or Process.waitFor() never 102 | * returning (as the buffer is full, pausing the native process)

103 | * 104 | * @param shell Name of the shell 105 | * @param inputStream InputStream to read from 106 | * @param outputList {@literal List} to write to, or null 107 | * @param logLevel The custom log level to use for logging the command output. If set to 108 | * {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used. 109 | */ 110 | @AnyThread 111 | public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, 112 | @Nullable List outputList, 113 | @Nullable Integer logLevel) { 114 | super("Gobbler#" + incThreadCounter()); 115 | this.shell = shell; 116 | this.inputStream = inputStream; 117 | reader = new BufferedReader(new InputStreamReader(inputStream)); 118 | streamClosedListener = null; 119 | 120 | listWriter = outputList; 121 | stringWriter = null; 122 | lineListener = null; 123 | 124 | mLogLevel = logLevel; 125 | } 126 | 127 | /** 128 | *

StreamGobbler constructor

129 | * 130 | *

We use this class because shell STDOUT and STDERR should be read as quickly as 131 | * possible to prevent a deadlock from occurring, or Process.waitFor() never 132 | * returning (as the buffer is full, pausing the native process)

133 | * Do not use this for concurrent reading for STDOUT and STDERR for the same StringBuilder since 134 | * its not synchronized. 135 | * 136 | * @param shell Name of the shell 137 | * @param inputStream InputStream to read from 138 | * @param outputString {@literal List} to write to, or null 139 | * @param logLevel The custom log level to use for logging the command output. If set to 140 | * {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used. 141 | */ 142 | @AnyThread 143 | public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, 144 | @Nullable StringBuilder outputString, 145 | @Nullable Integer logLevel) { 146 | super("Gobbler#" + incThreadCounter()); 147 | this.shell = shell; 148 | this.inputStream = inputStream; 149 | reader = new BufferedReader(new InputStreamReader(inputStream)); 150 | streamClosedListener = null; 151 | 152 | listWriter = null; 153 | stringWriter = outputString; 154 | lineListener = null; 155 | 156 | mLogLevel = logLevel; 157 | } 158 | 159 | /** 160 | *

StreamGobbler constructor

161 | * 162 | *

We use this class because shell STDOUT and STDERR should be read as quickly as 163 | * possible to prevent a deadlock from occurring, or Process.waitFor() never 164 | * returning (as the buffer is full, pausing the native process)

165 | * 166 | * @param shell Name of the shell 167 | * @param inputStream InputStream to read from 168 | * @param onLineListener OnLineListener callback 169 | * @param onStreamClosedListener OnStreamClosedListener callback 170 | * @param logLevel The custom log level to use for logging the command output. If set to 171 | * {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used. 172 | */ 173 | @AnyThread 174 | public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, 175 | @Nullable OnLineListener onLineListener, 176 | @Nullable OnStreamClosedListener onStreamClosedListener, 177 | @Nullable Integer logLevel) { 178 | super("Gobbler#" + incThreadCounter()); 179 | this.shell = shell; 180 | this.inputStream = inputStream; 181 | reader = new BufferedReader(new InputStreamReader(inputStream)); 182 | streamClosedListener = onStreamClosedListener; 183 | 184 | listWriter = null; 185 | stringWriter = null; 186 | lineListener = onLineListener; 187 | 188 | mLogLevel = logLevel; 189 | } 190 | 191 | @Override 192 | public void run() { 193 | String defaultLogTag = Logger.getDefaultLogTag(); 194 | boolean loggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(mLogLevel); 195 | if (loggingEnabled) 196 | Logger.logVerbose(LOG_TAG, "Using custom log level: " + mLogLevel + ", current log level: " + Logger.getLogLevel()); 197 | 198 | // keep reading the InputStream until it ends (or an error occurs) 199 | // optionally pausing when a command is executed that consumes the InputStream itself 200 | try { 201 | String line; 202 | while ((line = reader.readLine()) != null) { 203 | if (loggingEnabled) 204 | Logger.logVerboseForce(defaultLogTag + "Command", String.format(Locale.ENGLISH, "[%s] %s", shell, line)); // This will get truncated by LOGGER_ENTRY_MAX_LEN, likely 4KB 205 | 206 | if (stringWriter != null) stringWriter.append(line).append("\n"); 207 | if (listWriter != null) listWriter.add(line); 208 | if (lineListener != null) lineListener.onLine(line); 209 | while (!active) { 210 | synchronized (this) { 211 | try { 212 | this.wait(128); 213 | } catch (InterruptedException e) { 214 | // no action 215 | } 216 | } 217 | } 218 | } 219 | } catch (IOException e) { 220 | // reader probably closed, expected exit condition 221 | if (streamClosedListener != null) { 222 | calledOnClose = true; 223 | streamClosedListener.onStreamClosed(); 224 | } 225 | } 226 | 227 | // make sure our stream is closed and resources will be freed 228 | try { 229 | reader.close(); 230 | } catch (IOException e) { 231 | // read already closed 232 | } 233 | 234 | if (!calledOnClose) { 235 | if (streamClosedListener != null) { 236 | calledOnClose = true; 237 | streamClosedListener.onStreamClosed(); 238 | } 239 | } 240 | } 241 | 242 | /** 243 | *

Resume consuming the input from the stream

244 | */ 245 | @AnyThread 246 | public void resumeGobbling() { 247 | if (!active) { 248 | synchronized (this) { 249 | active = true; 250 | this.notifyAll(); 251 | } 252 | } 253 | } 254 | 255 | /** 256 | *

Suspend gobbling, so other code may read from the InputStream instead

257 | * 258 | *

This should only be called from the OnLineListener callback!

259 | */ 260 | @AnyThread 261 | public void suspendGobbling() { 262 | synchronized (this) { 263 | active = false; 264 | this.notifyAll(); 265 | } 266 | } 267 | 268 | /** 269 | *

Wait for gobbling to be suspended

270 | * 271 | *

Obviously this cannot be called from the same thread as {@link #suspendGobbling()}

272 | */ 273 | @WorkerThread 274 | public void waitForSuspend() { 275 | synchronized (this) { 276 | while (active) { 277 | try { 278 | this.wait(32); 279 | } catch (InterruptedException e) { 280 | // no action 281 | } 282 | } 283 | } 284 | } 285 | 286 | /** 287 | *

Is gobbling suspended ?

288 | * 289 | * @return is gobbling suspended? 290 | */ 291 | @AnyThread 292 | public boolean isSuspended() { 293 | synchronized (this) { 294 | return !active; 295 | } 296 | } 297 | 298 | /** 299 | *

Get current source InputStream

300 | * 301 | * @return source InputStream 302 | */ 303 | @NonNull 304 | @AnyThread 305 | public InputStream getInputStream() { 306 | return inputStream; 307 | } 308 | 309 | /** 310 | *

Get current OnLineListener

311 | * 312 | * @return OnLineListener 313 | */ 314 | @Nullable 315 | @AnyThread 316 | public OnLineListener getOnLineListener() { 317 | return lineListener; 318 | } 319 | 320 | void conditionalJoin() throws InterruptedException { 321 | if (calledOnClose) return; // deadlock from callback, we're inside exit procedure 322 | if (Thread.currentThread() == this) return; // can't join self 323 | join(); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/environment/AndroidShellEnvironment.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.environment; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import net.dinglisch.android.appfactory.utils.logger.Logger; 8 | import net.dinglisch.android.appfactory.utils.shell.ExecutionCommand; 9 | 10 | import java.io.File; 11 | import java.util.HashMap; 12 | 13 | /** 14 | * Environment for Android. 15 | * 16 | * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java 17 | * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:system/core/rootdir/init.environ.rc.in 18 | * https://cs.android.com/android/platform/superproject/+/android-5.0.0_r1.0.1:system/core/rootdir/init.environ.rc.in 19 | * https://cs.android.com/android/_/android/platform/system/core/+/refs/tags/android-12.0.0_r32:rootdir/init.rc;l=910 20 | * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:bionic/libc/include/paths.h;l=50 21 | * https://cs.android.com/android/_/android/platform/system/core/+/refs/tags/android-5.0.0_r1:rootdir/init.environ.rc.in;l=3 22 | */ 23 | public class AndroidShellEnvironment extends UnixShellEnvironment { 24 | private static final String LOG_TAG = "AndroidShellEnvironment"; 25 | 26 | protected Context mCurrentPackageContext; 27 | 28 | public AndroidShellEnvironment(@NonNull Context currentPackageContext) { 29 | mCurrentPackageContext = currentPackageContext.getApplicationContext(); 30 | } 31 | 32 | /** Get shell environment for Android. */ 33 | @NonNull 34 | @Override 35 | public HashMap getEnvironment(boolean isFailSafe) { 36 | HashMap environment = new HashMap<>(); 37 | 38 | environment.put(ENV_HOME, "/"); 39 | environment.put(ENV_LANG, "en_US.UTF-8"); 40 | environment.put(ENV_PATH, System.getenv(ENV_PATH)); 41 | environment.put(ENV_TMPDIR, "/data/local/tmp"); 42 | 43 | environment.put(ENV_COLORTERM, "truecolor"); 44 | environment.put(ENV_TERM, "xterm-256color"); 45 | 46 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_ASSETS"); 47 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_DATA"); 48 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_ROOT"); 49 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_STORAGE"); 50 | 51 | // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least 52 | // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. 53 | // https://cs.android.com/android/_/android/platform/system/core/+/fc000489 54 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "EXTERNAL_STORAGE"); 55 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ASEC_MOUNTPOINT"); 56 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "LOOP_MOUNTPOINT"); 57 | 58 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_RUNTIME_ROOT"); 59 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_ART_ROOT"); 60 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_I18N_ROOT"); 61 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "ANDROID_TZDATA_ROOT"); 62 | 63 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "BOOTCLASSPATH"); 64 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "DEX2OATBOOTCLASSPATH"); 65 | ShellEnvironmentUtils.putToEnvIfInSystemEnv(environment, "SYSTEMSERVERCLASSPATH"); 66 | 67 | return environment; 68 | } 69 | 70 | 71 | 72 | @NonNull 73 | @Override 74 | public String getDefaultWorkingDirectoryPath() { 75 | return "/"; 76 | } 77 | 78 | 79 | @NonNull 80 | @Override 81 | public String getDefaultBinPath() { 82 | return "/system/bin"; 83 | } 84 | 85 | @NonNull 86 | @Override 87 | public HashMap setupShellCommandEnvironment(@NonNull ExecutionCommand executionCommand) { 88 | Logger.logInfo(LOG_TAG, "time:start"); 89 | 90 | HashMap environment = getEnvironment(executionCommand.isFailsafe); 91 | Logger.logInfo(LOG_TAG, "time:end"); 92 | 93 | String workingDirectory = executionCommand.workingDirectory; 94 | environment.put(ENV_PWD, 95 | workingDirectory != null && !workingDirectory.isEmpty() ? new File(workingDirectory).getAbsolutePath() : // PWD must be absolute path 96 | getDefaultWorkingDirectoryPath()); 97 | ShellEnvironmentUtils.createHomeDir(environment); 98 | 99 | return environment; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/environment/AppShellEnvironment.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.environment; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.os.Build; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import net.dinglisch.android.appfactory.AppFactoryConstants; 10 | import net.dinglisch.android.appfactory.utils.android.PackageUtils; 11 | 12 | import java.util.HashMap; 13 | 14 | /** 15 | * Environment for the app. 16 | */ 17 | public class AppShellEnvironment extends AndroidShellEnvironment { 18 | 19 | /** Environment variable for the app prefix path. */ 20 | public static final String ENV_PREFIX = "PREFIX"; // Default: "PREFIX" 21 | 22 | 23 | 24 | @SuppressLint("SdCardPath") 25 | public AppShellEnvironment(@NonNull Context currentPackageContext) { 26 | super(currentPackageContext); 27 | } 28 | 29 | /** Get shell environment for the app. */ 30 | @NonNull 31 | @Override 32 | public HashMap getEnvironment(boolean isFailSafe) { 33 | 34 | // App environment builds upon the Android environment 35 | HashMap environment = super.getEnvironment(isFailSafe); 36 | 37 | environment.put(ENV_HOME, AppFactoryConstants.HOME_DIR_PATH); 38 | environment.put(ENV_PREFIX, AppFactoryConstants.PREFIX_DIR_PATH); 39 | 40 | // If failsafe is not enabled, then we keep default PATH and TMPDIR so that system binaries can be used 41 | if (!isFailSafe) { 42 | environment.put(ENV_TMPDIR, AppFactoryConstants.TMP_PREFIX_DIR_PATH); 43 | environment.put(ENV_PATH, AppFactoryConstants.BIN_PREFIX_DIR_PATH); 44 | 45 | // App binaries on Android 7+ rely on DT_RUNPATH, so LD_LIBRARY_PATH should be unset by default 46 | // Secondary user will have different prefix than /data/data//files/usr 47 | // and so requires LD_LIBRARY_PATH, otherwise will get "CANNOT LINK EXECUTABLE... library not found" errors 48 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !PackageUtils.isCurrentUserThePrimaryUser(mCurrentPackageContext)) { 49 | environment.put(ENV_LD_LIBRARY_PATH, AppFactoryConstants.LIB_PREFIX_DIR_PATH); 50 | } else { 51 | environment.remove(ENV_LD_LIBRARY_PATH); 52 | } 53 | } 54 | 55 | return environment; 56 | } 57 | 58 | 59 | 60 | 61 | 62 | @NonNull 63 | @Override 64 | public String getDefaultWorkingDirectoryPath() { 65 | return AppFactoryConstants.HOME_DIR_PATH; 66 | } 67 | 68 | @NonNull 69 | @Override 70 | public String getDefaultBinPath() { 71 | return AppFactoryConstants.BIN_PREFIX_DIR_PATH; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/environment/IShellEnvironment.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.environment; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import net.dinglisch.android.appfactory.utils.shell.ExecutionCommand; 9 | 10 | import java.util.HashMap; 11 | 12 | public interface IShellEnvironment { 13 | 14 | /** 15 | * Get the default working directory path for the environment in case the path that was passed 16 | * was {@code null} or empty. 17 | * 18 | * @return Should return the default working directory path. 19 | */ 20 | @NonNull 21 | String getDefaultWorkingDirectoryPath(); 22 | 23 | /** 24 | * Get the default "/bin" path, like $PREFIX/bin. 25 | * 26 | * @return Should return the "/bin" path. 27 | */ 28 | @NonNull 29 | String getDefaultBinPath(); 30 | 31 | /** 32 | * Setup shell command arguments for the file to execute, like interpreter, etc. 33 | * 34 | * @param fileToExecute The file to execute. 35 | * @param arguments The arguments to pass to the executable. 36 | * @return Should return the final process arguments. 37 | */ 38 | @NonNull 39 | String[] setupShellCommandArguments(@NonNull String fileToExecute, @Nullable String[] arguments); 40 | 41 | /** 42 | * Setup shell command environment to be used for commands. 43 | * 44 | * @param executionCommand The {@link ExecutionCommand} for which to set environment. 45 | * @return Should return the shell environment. 46 | */ 47 | @NonNull 48 | HashMap setupShellCommandEnvironment(@NonNull ExecutionCommand executionCommand); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/environment/ShellEnvironmentUtils.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.environment; 2 | 3 | import static net.dinglisch.android.appfactory.utils.shell.environment.UnixShellEnvironment.ENV_HOME; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import net.dinglisch.android.appfactory.utils.logger.Logger; 9 | 10 | import java.io.File; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class ShellEnvironmentUtils { 17 | 18 | private static final String LOG_TAG = "ShellEnvironmentUtils"; 19 | 20 | /** 21 | * Convert environment {@link HashMap} to `environ` {@link List }. 22 | * 23 | * The items in the environ will have the format `name=value`. 24 | * 25 | * Check {@link #isValidEnvironmentVariableName(String)} and {@link #isValidEnvironmentVariableValue(String)} 26 | * for valid variable names and values. 27 | * 28 | * https://manpages.debian.org/testing/manpages/environ.7.en.html 29 | * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html 30 | */ 31 | @NonNull 32 | public static List convertEnvironmentToEnviron(@NonNull HashMap environmentMap) { 33 | List environmentList = new ArrayList<>(environmentMap.size()); 34 | String value; 35 | for (String name : environmentMap.keySet()) { 36 | value = environmentMap.get(name); 37 | if (isValidEnvironmentVariableNameValuePair(name, value, true)) 38 | environmentList.add(name + "=" + environmentMap.get(name)); 39 | } 40 | return environmentList; 41 | } 42 | 43 | /** 44 | * Convert environment {@link HashMap} to {@link List< ShellEnvironmentVariable >}. Each item 45 | * will have its {@link ShellEnvironmentVariable#escaped} set to {@code false}. 46 | */ 47 | @NonNull 48 | public static List convertEnvironmentMapToEnvironmentVariableList(@NonNull HashMap environmentMap) { 49 | List environmentList = new ArrayList<>(); 50 | for (String name :environmentMap.keySet()) { 51 | environmentList.add(new ShellEnvironmentVariable(name, environmentMap.get(name), false)); 52 | } 53 | return environmentList; 54 | } 55 | 56 | /** 57 | * Check if environment variable name and value pair is valid. Errors will be logged if 58 | * {@code logErrors} is {@code true}. 59 | * 60 | * Check {@link #isValidEnvironmentVariableName(String)} and {@link #isValidEnvironmentVariableValue(String)} 61 | * for valid variable names and values. 62 | */ 63 | public static boolean isValidEnvironmentVariableNameValuePair(@Nullable String name, @Nullable String value, boolean logErrors) { 64 | if (!isValidEnvironmentVariableName(name)) { 65 | if (logErrors) 66 | Logger.logErrorPrivate(LOG_TAG, "Invalid environment variable name. name=`" + name + "`, value=`" + value + "`"); 67 | return false; 68 | } 69 | 70 | if (!isValidEnvironmentVariableValue(value)) { 71 | if (logErrors) 72 | Logger.logErrorPrivate(LOG_TAG, "Invalid environment variable value. name=`" + name + "`, value=`" + value + "`"); 73 | return false; 74 | } 75 | 76 | return true; 77 | } 78 | 79 | /** 80 | * Check if environment variable name is valid. It must not be {@code null} and must not contain 81 | * the null byte ('\0') and must only contain alphanumeric and underscore characters and must not 82 | * start with a digit. 83 | */ 84 | public static boolean isValidEnvironmentVariableName(@Nullable String name) { 85 | return name != null && !name.contains("\0") && name.matches("[a-zA-Z_][a-zA-Z0-9_]*"); 86 | } 87 | 88 | /** 89 | * Check if environment variable value is valid. It must not be {@code null} and must not contain 90 | * the null byte ('\0'). 91 | */ 92 | public static boolean isValidEnvironmentVariableValue(@Nullable String value) { 93 | return value != null && !value.contains("\0"); 94 | } 95 | 96 | 97 | 98 | /** Put value in environment if variable exists in {@link System) environment. */ 99 | public static void putToEnvIfInSystemEnv(@NonNull HashMap environment, 100 | @NonNull String name) { 101 | String value = System.getenv(name); 102 | if (value != null) { 103 | environment.put(name, value); 104 | } 105 | } 106 | 107 | /** Put {@link String} value in environment if value set. */ 108 | public static void putToEnvIfSet(@NonNull HashMap environment, @NonNull String name, 109 | @Nullable String value) { 110 | if (value != null) { 111 | environment.put(name, value); 112 | } 113 | } 114 | 115 | /** Put {@link Boolean} value "true" or "false" in environment if value set. */ 116 | public static void putToEnvIfSet(@NonNull HashMap environment, @NonNull String name, 117 | @Nullable Boolean value) { 118 | if (value != null) { 119 | environment.put(name, String.valueOf(value)); 120 | } 121 | } 122 | 123 | 124 | 125 | /** Create HOME directory in environment {@link Map} if set. */ 126 | public static void createHomeDir(@NonNull HashMap environment) { 127 | String homeDirectoryPath = environment.get(ENV_HOME); 128 | if (homeDirectoryPath != null && !homeDirectoryPath.isEmpty()) { 129 | File homeDirectory = new File(homeDirectoryPath); 130 | if (!homeDirectory.exists() && !homeDirectory.mkdirs()) { 131 | Logger.logErrorExtended(LOG_TAG, "Failed to create shell home directory \"" + homeDirectoryPath + "\""); 132 | } 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/environment/ShellEnvironmentVariable.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.environment; 2 | 3 | public class ShellEnvironmentVariable implements Comparable { 4 | 5 | /** The name for environment variable */ 6 | public String name; 7 | 8 | /** The value for environment variable */ 9 | public String value; 10 | 11 | /** If environment variable {@link #value} is already escaped. */ 12 | public boolean escaped; 13 | 14 | public ShellEnvironmentVariable(String name, String value) { 15 | this(name, value, false); 16 | } 17 | 18 | public ShellEnvironmentVariable(String name, String value, boolean escaped) { 19 | this.name = name; 20 | this.value = value; 21 | this.escaped = escaped; 22 | } 23 | 24 | @Override 25 | public int compareTo(ShellEnvironmentVariable other) { 26 | return this.name.compareTo(other.name); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/environment/UnixShellEnvironment.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.environment; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import net.dinglisch.android.appfactory.utils.shell.ExecutionCommand; 7 | import net.dinglisch.android.appfactory.utils.shell.ShellUtils; 8 | 9 | import java.util.HashMap; 10 | 11 | /** 12 | * Environment for Unix-like systems. 13 | * 14 | * https://manpages.debian.org/testing/manpages/environ.7.en.html 15 | * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html 16 | */ 17 | public abstract class UnixShellEnvironment implements IShellEnvironment { 18 | 19 | /** Environment variable for the terminal's colour capabilities. */ 20 | public static final String ENV_COLORTERM = "COLORTERM"; 21 | 22 | /** Environment variable for the path of the user's home directory. */ 23 | public static final String ENV_HOME = "HOME"; 24 | 25 | /** Environment variable for the locale category for native language, local customs, and coded 26 | * character set in the absence of the LC_ALL and other LC_* environment variables. */ 27 | public static final String ENV_LANG = "LANG"; 28 | 29 | /** Environment variable for the represent the sequence of directory paths separated with 30 | * colons ":" that should be searched in for dynamic shared libraries to link programs against. */ 31 | public static final String ENV_LD_LIBRARY_PATH = "LD_LIBRARY_PATH"; 32 | 33 | /** Environment variable for the represent the sequence of directory path prefixes separated with 34 | * colons ":" that certain functions and utilities apply in searching for an executable file 35 | * known only by a filename. */ 36 | public static final String ENV_PATH = "PATH"; 37 | 38 | /** Environment variable for the absolute path of the current working directory. It shall not 39 | * contain any components that are dot or dot-dot. The value is set by the cd utility, and by 40 | * the sh utility during initialization. */ 41 | public static final String ENV_PWD = "PWD"; 42 | 43 | /** Environment variable for the terminal type for which output is to be prepared. This information 44 | * is used by utilities and application programs wishing to exploit special capabilities specific 45 | * to a terminal. The format and allowable values of this environment variable are unspecified. */ 46 | public static final String ENV_TERM = "TERM"; 47 | 48 | /** Environment variable for the path of a directory made available for programs that need a place 49 | * to create temporary files. */ 50 | public static final String ENV_TMPDIR = "TMPDIR"; 51 | 52 | 53 | /** Names for common/supported login shell binaries. */ 54 | public static final String[] LOGIN_SHELL_BINARIES = new String[]{"login", "bash", "zsh", "fish", "sh"}; 55 | 56 | 57 | 58 | @NonNull 59 | public abstract HashMap getEnvironment(boolean isFailSafe); 60 | 61 | @NonNull 62 | @Override 63 | public abstract String getDefaultWorkingDirectoryPath(); 64 | 65 | @NonNull 66 | @Override 67 | public abstract String getDefaultBinPath(); 68 | 69 | @NonNull 70 | @Override 71 | public String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) { 72 | return ShellUtils.setupShellCommandArguments(executable, arguments); 73 | } 74 | 75 | @NonNull 76 | @Override 77 | public abstract HashMap setupShellCommandEnvironment(@NonNull ExecutionCommand executionCommand); 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/result/ResultData.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.result; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.io.Serializable; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import net.dinglisch.android.appfactory.utils.data.DataUtils; 11 | import net.dinglisch.android.appfactory.utils.errors.Errno; 12 | import net.dinglisch.android.appfactory.utils.errors.Error; 13 | import net.dinglisch.android.appfactory.utils.logger.Logger; 14 | import net.dinglisch.android.appfactory.utils.markdown.MarkdownUtils; 15 | 16 | public class ResultData implements Serializable { 17 | 18 | /** The stdout of command. */ 19 | public final StringBuilder stdout = new StringBuilder(); 20 | /** The stderr of command. */ 21 | public final StringBuilder stderr = new StringBuilder(); 22 | /** The exit code of command. */ 23 | public Integer exitCode; 24 | 25 | /** The internal errors list of command. */ 26 | public List errorsList = new ArrayList<>(); 27 | 28 | 29 | public ResultData() { 30 | } 31 | 32 | 33 | public void clearStdout() { 34 | stdout.setLength(0); 35 | } 36 | 37 | public StringBuilder prependStdout(String message) { 38 | return stdout.insert(0, message); 39 | } 40 | 41 | public StringBuilder prependStdoutLn(String message) { 42 | return stdout.insert(0, message + "\n"); 43 | } 44 | 45 | public StringBuilder appendStdout(String message) { 46 | return stdout.append(message); 47 | } 48 | 49 | public StringBuilder appendStdoutLn(String message) { 50 | return stdout.append(message).append("\n"); 51 | } 52 | 53 | 54 | public void clearStderr() { 55 | stderr.setLength(0); 56 | } 57 | 58 | public StringBuilder prependStderr(String message) { 59 | return stderr.insert(0, message); 60 | } 61 | 62 | public StringBuilder prependStderrLn(String message) { 63 | return stderr.insert(0, message + "\n"); 64 | } 65 | 66 | public StringBuilder appendStderr(String message) { 67 | return stderr.append(message); 68 | } 69 | 70 | public StringBuilder appendStderrLn(String message) { 71 | return stderr.append(message).append("\n"); 72 | } 73 | 74 | 75 | public synchronized boolean setStateFailed(@NonNull Error error) { 76 | return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null); 77 | } 78 | 79 | public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) { 80 | return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable)); 81 | } 82 | public synchronized boolean setStateFailed(@NonNull Error error, List throwablesList) { 83 | return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList); 84 | } 85 | 86 | public synchronized boolean setStateFailed(int code, String message) { 87 | return setStateFailed(null, code, message, null); 88 | } 89 | 90 | public synchronized boolean setStateFailed(int code, String message, Throwable throwable) { 91 | return setStateFailed(null, code, message, Collections.singletonList(throwable)); 92 | } 93 | 94 | public synchronized boolean setStateFailed(int code, String message, List throwablesList) { 95 | return setStateFailed(null, code, message, throwablesList); 96 | } 97 | 98 | public synchronized boolean setStateFailed(String type, int code, String message, List throwablesList) { 99 | if (errorsList == null) 100 | errorsList = new ArrayList<>(); 101 | 102 | Error error = new Error(); 103 | errorsList.add(error); 104 | 105 | return error.setStateFailed(type, code, message, throwablesList); 106 | } 107 | 108 | public boolean isStateFailed() { 109 | if (errorsList != null) { 110 | for (Error error : errorsList) 111 | if (error.isStateFailed()) 112 | return true; 113 | } 114 | 115 | return false; 116 | } 117 | 118 | public int getErrCode() { 119 | if (errorsList != null && errorsList.size() > 0) 120 | return errorsList.get(errorsList.size() - 1).getCode(); 121 | else 122 | return Errno.ERRNO_SUCCESS.getCode(); 123 | } 124 | 125 | 126 | @NonNull 127 | @Override 128 | public String toString() { 129 | return getResultDataLogString(this, true); 130 | } 131 | 132 | /** 133 | * Get a log friendly {@link String} for {@link ResultData} parameters. 134 | * 135 | * @param resultData The {@link ResultData} to convert. 136 | * @param logStdoutAndStderr Set to {@code true} if {@link #stdout} and {@link #stderr} should be logged. 137 | * @return Returns the log friendly {@link String}. 138 | */ 139 | public static String getResultDataLogString(final ResultData resultData, boolean logStdoutAndStderr) { 140 | if (resultData == null) return "null"; 141 | 142 | StringBuilder logString = new StringBuilder(); 143 | 144 | if (logStdoutAndStderr) { 145 | logString.append("\n").append(resultData.getStdoutLogString()); 146 | logString.append("\n").append(resultData.getStderrLogString()); 147 | } 148 | logString.append("\n").append(resultData.getExitCodeLogString()); 149 | 150 | logString.append("\n\n").append(getErrorsListLogString(resultData)); 151 | 152 | return logString.toString(); 153 | } 154 | 155 | 156 | 157 | public String getStdoutLogString() { 158 | if (stdout.toString().isEmpty()) 159 | return Logger.getSingleLineLogStringEntry("Stdout", null, "-"); 160 | else 161 | return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-"); 162 | } 163 | 164 | public String getStderrLogString() { 165 | if (stderr.toString().isEmpty()) 166 | return Logger.getSingleLineLogStringEntry("Stderr", null, "-"); 167 | else 168 | return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-"); 169 | } 170 | 171 | public String getExitCodeLogString() { 172 | return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-"); 173 | } 174 | 175 | public static String getErrorsListLogString(final ResultData resultData) { 176 | if (resultData == null) return "null"; 177 | 178 | StringBuilder logString = new StringBuilder(); 179 | 180 | if (resultData.errorsList != null) { 181 | for (Error error : resultData.errorsList) { 182 | if (error.isStateFailed()) { 183 | if (!logString.toString().isEmpty()) 184 | logString.append("\n"); 185 | logString.append(Error.getErrorLogString(error)); 186 | } 187 | } 188 | } 189 | 190 | return logString.toString(); 191 | } 192 | 193 | /** 194 | * Get a markdown {@link String} for {@link ResultData}. 195 | * 196 | * @param resultData The {@link ResultData} to convert. 197 | * @return Returns the markdown {@link String}. 198 | */ 199 | public static String getResultDataMarkdownString(final ResultData resultData) { 200 | if (resultData == null) return "null"; 201 | 202 | StringBuilder markdownString = new StringBuilder(); 203 | 204 | if (resultData.stdout.toString().isEmpty()) 205 | markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stdout", null, "-")); 206 | else 207 | markdownString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", resultData.stdout.toString(), "-")); 208 | 209 | if (resultData.stderr.toString().isEmpty()) 210 | markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stderr", null, "-")); 211 | else 212 | markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", resultData.stderr.toString(), "-")); 213 | 214 | markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", resultData.exitCode, "-")); 215 | 216 | markdownString.append("\n\n").append(getErrorsListMarkdownString(resultData)); 217 | 218 | 219 | return markdownString.toString(); 220 | } 221 | 222 | public static String getErrorsListMarkdownString(final ResultData resultData) { 223 | if (resultData == null) return "null"; 224 | 225 | StringBuilder markdownString = new StringBuilder(); 226 | 227 | if (resultData.errorsList != null) { 228 | for (Error error : resultData.errorsList) { 229 | if (error.isStateFailed()) { 230 | if (!markdownString.toString().isEmpty()) 231 | markdownString.append("\n"); 232 | markdownString.append(Error.getErrorMarkdownString(error)); 233 | } 234 | } 235 | } 236 | 237 | return markdownString.toString(); 238 | } 239 | 240 | public static String getErrorsListMinimalString(final ResultData resultData) { 241 | if (resultData == null) return "null"; 242 | 243 | StringBuilder minimalString = new StringBuilder(); 244 | 245 | if (resultData.errorsList != null) { 246 | for (Error error : resultData.errorsList) { 247 | if (error.isStateFailed()) { 248 | if (!minimalString.toString().isEmpty()) 249 | minimalString.append("\n"); 250 | minimalString.append(Error.getMinimalErrorString(error)); 251 | } 252 | } 253 | } 254 | 255 | return minimalString.toString(); 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /app/src/main/java/net/dinglisch/android/appfactory/utils/shell/runner/app/AppShell.java: -------------------------------------------------------------------------------- 1 | package net.dinglisch.android.appfactory.utils.shell.runner.app; 2 | 3 | import android.content.Context; 4 | import android.system.ErrnoException; 5 | import android.system.Os; 6 | import android.system.OsConstants; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | 11 | import com.google.common.base.Joiner; 12 | 13 | import net.dinglisch.android.appfactory.R; 14 | import net.dinglisch.android.appfactory.utils.data.DataUtils; 15 | import net.dinglisch.android.appfactory.utils.errors.Errno; 16 | import net.dinglisch.android.appfactory.utils.logger.Logger; 17 | import net.dinglisch.android.appfactory.utils.shell.ExecutionCommand; 18 | import net.dinglisch.android.appfactory.utils.shell.ExecutionCommand.ExecutionState; 19 | import net.dinglisch.android.appfactory.utils.shell.ShellUtils; 20 | import net.dinglisch.android.appfactory.utils.shell.environment.IShellEnvironment; 21 | import net.dinglisch.android.appfactory.utils.shell.StreamGobbler; 22 | import net.dinglisch.android.appfactory.utils.shell.environment.ShellEnvironmentUtils; 23 | import net.dinglisch.android.appfactory.utils.shell.result.ResultData; 24 | 25 | import java.io.DataOutputStream; 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.Collections; 30 | import java.util.HashMap; 31 | import java.util.List; 32 | 33 | /** 34 | * A class that maintains info for background app shells run with {@link Runtime#exec(String[], String[], File)}. 35 | * It also provides a way to link each {@link Process} with the {@link ExecutionCommand} 36 | * that started it. The shell is run in the app user context. 37 | */ 38 | public final class AppShell { 39 | 40 | private final Process mProcess; 41 | private final ExecutionCommand mExecutionCommand; 42 | private final AppShellClient mAppShellClient; 43 | 44 | private static final String LOG_TAG = "AppShell"; 45 | 46 | private AppShell(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand, 47 | final AppShellClient appShellClient) { 48 | this.mProcess = process; 49 | this.mExecutionCommand = executionCommand; 50 | this.mAppShellClient = appShellClient; 51 | } 52 | 53 | /** 54 | * Start execution of an {@link ExecutionCommand} with {@link Runtime#exec(String[], String[], File)}. 55 | * 56 | * The {@link ExecutionCommand#executable}, must be set. 57 | * The {@link ExecutionCommand#commandLabel}, {@link ExecutionCommand#arguments} and 58 | * {@link ExecutionCommand#workingDirectory} may optionally be set. 59 | * 60 | * @param currentPackageContext The {@link Context} for operations. This must be the context for 61 | * the current package and not the context of a `sharedUserId` package, 62 | * since environment setup may be dependent on current package. 63 | * @param executionCommand The {@link ExecutionCommand} containing the information for execution command. 64 | * @param appShellClient The {@link AppShellClient} interface implementation. 65 | * The {@link AppShellClient#onAppShellExited(AppShell)} will 66 | * be called regardless of {@code isSynchronous} value but not if 67 | * {@code null} is returned by this method. This can 68 | * optionally be {@code null}. 69 | * @param shellEnvironmentClient The {@link IShellEnvironment} interface implementation. 70 | * @param additionalEnvironment The additional shell environment variables to export. Existing 71 | * variables will be overridden. 72 | * @param isSynchronous If set to {@code true}, then the command will be executed in the 73 | * caller thread and results returned synchronously in the {@link ExecutionCommand} 74 | * sub object of the {@link AppShell} returned. 75 | * If set to {@code false}, then a new thread is started run the commands 76 | * asynchronously in the background and control is returned to the caller thread. 77 | * @return Returns the {@link AppShell}. This will be {@code null} if failed to start the execution command. 78 | */ 79 | public static AppShell execute(@NonNull final Context currentPackageContext, @NonNull ExecutionCommand executionCommand, 80 | final AppShellClient appShellClient, 81 | @NonNull final IShellEnvironment shellEnvironmentClient, 82 | @Nullable HashMap additionalEnvironment, 83 | final boolean isSynchronous) { 84 | if (executionCommand.executable == null || executionCommand.executable.isEmpty()) { 85 | executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), 86 | currentPackageContext.getString(R.string.error_executable_unset, executionCommand.getCommandIdAndLabelLogString())); 87 | AppShell.processAppShellResult(null, executionCommand); 88 | return null; 89 | } 90 | 91 | if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) 92 | executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath(); 93 | if (executionCommand.workingDirectory.isEmpty()) 94 | executionCommand.workingDirectory = "/"; 95 | 96 | // Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh". 97 | String executableBasename = ShellUtils.getExecutableBasename(executionCommand.executable); 98 | 99 | if (executionCommand.shellName == null) 100 | executionCommand.shellName = executableBasename; 101 | 102 | if (executionCommand.commandLabel == null) 103 | executionCommand.commandLabel = executableBasename; 104 | 105 | // Setup command args 106 | final String[] commandArray = shellEnvironmentClient.setupShellCommandArguments(executionCommand.executable, executionCommand.arguments); 107 | 108 | // Setup command environment 109 | HashMap environment = shellEnvironmentClient.setupShellCommandEnvironment(executionCommand); 110 | if (additionalEnvironment != null) 111 | environment.putAll(additionalEnvironment); 112 | List environmentList = ShellEnvironmentUtils.convertEnvironmentToEnviron(environment); 113 | Collections.sort(environmentList); 114 | String[] environmentArray = environmentList.toArray(new String[0]); 115 | 116 | if (!executionCommand.setState(ExecutionState.EXECUTING)) { 117 | executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), currentPackageContext.getString(R.string.error_failed_to_execute_app_shell_command, executionCommand.getCommandIdAndLabelLogString())); 118 | AppShell.processAppShellResult(null, executionCommand); 119 | return null; 120 | } 121 | 122 | // No need to log stdin if logging is disabled, like for app internal scripts 123 | Logger.logDebugExtended(LOG_TAG, ExecutionCommand.getExecutionInputLogString(executionCommand, 124 | true, Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel))); 125 | Logger.logVerboseExtended(LOG_TAG, "\"" + executionCommand.getCommandIdAndLabelLogString() + "\" AppShell Environment:\n" + 126 | Joiner.on("\n").join(environmentArray)); 127 | 128 | // Exec the process 129 | final Process process; 130 | try { 131 | process = Runtime.getRuntime().exec(commandArray, environmentArray, new File(executionCommand.workingDirectory)); 132 | } catch (IOException e) { 133 | executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), currentPackageContext.getString(R.string.error_failed_to_execute_app_shell_command, executionCommand.getCommandIdAndLabelLogString()), e); 134 | AppShell.processAppShellResult(null, executionCommand); 135 | return null; 136 | } 137 | 138 | final AppShell appShell = new AppShell(process, executionCommand, appShellClient); 139 | if (isSynchronous) { 140 | try { 141 | appShell.executeInner(currentPackageContext); 142 | } catch (IllegalThreadStateException | InterruptedException e) { 143 | // TODO: Should either of these be handled or returned? 144 | } 145 | } else { 146 | new Thread() { 147 | @Override 148 | public void run() { 149 | try { 150 | appShell.executeInner(currentPackageContext); 151 | } catch (IllegalThreadStateException | InterruptedException e) { 152 | // TODO: Should either of these be handled or returned? 153 | } 154 | } 155 | }.start(); 156 | } 157 | 158 | return appShell; 159 | } 160 | 161 | /** 162 | * Sets up stdout and stderr readers for the {@link #mProcess} and waits for the process to end. 163 | * 164 | * If the processes finishes, then sets {@link ResultData#stdout}, {@link ResultData#stderr} 165 | * and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code appShell} 166 | * and then calls {@link #processAppShellResult(AppShell, ExecutionCommand) to process the result}. 167 | * 168 | * @param context The {@link Context} for operations. 169 | */ 170 | private void executeInner(@NonNull final Context context) throws IllegalThreadStateException, InterruptedException { 171 | mExecutionCommand.mPid = ShellUtils.getPid(mProcess); 172 | 173 | Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell with pid " + mExecutionCommand.mPid); 174 | 175 | mExecutionCommand.resultData.exitCode = null; 176 | 177 | // setup stdin, and stdout and stderr gobblers 178 | DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream()); 179 | StreamGobbler STDOUT = new StreamGobbler(mExecutionCommand.mPid + "-stdout", mProcess.getInputStream(), mExecutionCommand.resultData.stdout, mExecutionCommand.backgroundCustomLogLevel); 180 | StreamGobbler STDERR = new StreamGobbler(mExecutionCommand.mPid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr, mExecutionCommand.backgroundCustomLogLevel); 181 | 182 | // start gobbling 183 | STDOUT.start(); 184 | STDERR.start(); 185 | 186 | if (!DataUtils.isNullOrEmpty(mExecutionCommand.stdin)) { 187 | try { 188 | STDIN.write((mExecutionCommand.stdin + "\n").getBytes(StandardCharsets.UTF_8)); 189 | STDIN.flush(); 190 | STDIN.close(); 191 | //STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8)); 192 | //STDIN.flush(); 193 | } catch(IOException e) { 194 | if (e.getMessage() != null && (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed"))) { 195 | // Method most horrid to catch broken pipe, in which case we 196 | // do nothing. The command is not a shell, the shell closed 197 | // STDIN, the script already contained the exit command, etc. 198 | // these cases we want the output instead of returning null. 199 | } else { 200 | // other issues we don't know how to handle, leads to 201 | // returning null 202 | mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_exception_received_while_executing_app_shell_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e); 203 | mExecutionCommand.resultData.exitCode = 1; 204 | AppShell.processAppShellResult(this, null); 205 | kill(); 206 | return; 207 | } 208 | } 209 | } 210 | 211 | // wait for our process to finish, while we gobble away in the background 212 | int exitCode = mProcess.waitFor(); 213 | 214 | // make sure our threads are done gobbling 215 | // and the process is destroyed - while the latter shouldn't be 216 | // needed in theory, and may even produce warnings, in "normal" Java 217 | // they are required for guaranteed cleanup of resources, so lets be 218 | // safe and do this on Android as well 219 | try { 220 | STDIN.close(); 221 | } catch (IOException e) { 222 | // might be closed already 223 | } 224 | STDOUT.join(); 225 | STDERR.join(); 226 | mProcess.destroy(); 227 | 228 | // Process result 229 | if (exitCode == 0) 230 | Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell with pid " + mExecutionCommand.mPid + " exited normally"); 231 | else 232 | Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell with pid " + mExecutionCommand.mPid + " exited with code: " + exitCode); 233 | 234 | // If the execution command has already failed, like SIGKILL was sent, then don't continue 235 | if (mExecutionCommand.isStateFailed()) { 236 | Logger.logDebug(LOG_TAG, "Ignoring setting \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell state to ExecutionState.EXECUTED and processing results since it has already failed"); 237 | return; 238 | } 239 | 240 | mExecutionCommand.resultData.exitCode = exitCode; 241 | 242 | if (!mExecutionCommand.setState(ExecutionState.EXECUTED)) 243 | return; 244 | 245 | AppShell.processAppShellResult(this, null); 246 | } 247 | 248 | /** 249 | * Kill this {@link AppShell} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess} 250 | * if its still executing. 251 | * 252 | * @param context The {@link Context} for operations. 253 | * @param processResult If set to {@code true}, then the {@link #processAppShellResult(AppShell, ExecutionCommand)} 254 | * will be called to process the failure. 255 | */ 256 | public void killIfExecuting(@NonNull final Context context, boolean processResult) { 257 | // If execution command has already finished executing, then no need to process results or send SIGKILL 258 | if (mExecutionCommand.hasExecuted()) { 259 | Logger.logDebug(LOG_TAG, "Ignoring sending SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell since it has already finished executing"); 260 | return; 261 | } 262 | 263 | Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell"); 264 | 265 | if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) { 266 | if (processResult) { 267 | mExecutionCommand.resultData.exitCode = 137; // SIGKILL 268 | AppShell.processAppShellResult(this, null); 269 | } 270 | } 271 | 272 | if (mExecutionCommand.isExecuting()) { 273 | kill(); 274 | } 275 | } 276 | 277 | /** 278 | * Kill this {@link AppShell} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}. 279 | */ 280 | public void kill() { 281 | int pid = ShellUtils.getPid(mProcess); 282 | try { 283 | // Send SIGKILL to process 284 | Os.kill(pid, OsConstants.SIGKILL); 285 | } catch (ErrnoException e) { 286 | Logger.logWarn(LOG_TAG, "Failed to send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" AppShell with pid " + pid + ": " + e.getMessage()); 287 | } 288 | } 289 | 290 | /** 291 | * Process the results of {@link AppShell} or {@link ExecutionCommand}. 292 | * 293 | * Only one of {@code appShell} and {@code executionCommand} must be set. 294 | * 295 | * If the {@code appShell} and its {@link #mAppShellClient} are not {@code null}, 296 | * then the {@link AppShellClient#onAppShellExited(AppShell)} callback will be called. 297 | * 298 | * @param appShell The {@link AppShell}, which should be set if 299 | * {@link #execute(Context, ExecutionCommand, AppShellClient, IShellEnvironment, HashMap, boolean)} 300 | * successfully started the process. 301 | * @param executionCommand The {@link ExecutionCommand}, which should be set if 302 | * {@link #execute(Context, ExecutionCommand, AppShellClient, IShellEnvironment, HashMap, boolean)} 303 | * failed to start the process. 304 | */ 305 | private static void processAppShellResult(final AppShell appShell, ExecutionCommand executionCommand) { 306 | if (appShell != null) 307 | executionCommand = appShell.mExecutionCommand; 308 | 309 | if (executionCommand == null) return; 310 | 311 | if (executionCommand.shouldNotProcessResults()) { 312 | Logger.logDebug(LOG_TAG, "Ignoring duplicate call to process \"" + executionCommand.getCommandIdAndLabelLogString() + "\" AppShell result"); 313 | return; 314 | } 315 | 316 | Logger.logDebug(LOG_TAG, "Processing \"" + executionCommand.getCommandIdAndLabelLogString() + "\" AppShell result"); 317 | 318 | if (appShell != null && appShell.mAppShellClient != null) { 319 | appShell.mAppShellClient.onAppShellExited(appShell); 320 | } else { 321 | // If a callback is not set and execution command didn't fail, then we set success state now 322 | // Otherwise, the callback host can set it himself when its done with the appShell 323 | if (!executionCommand.isStateFailed()) 324 | executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS); 325 | } 326 | } 327 | 328 | public Process getProcess() { 329 | return mProcess; 330 | } 331 | 332 | public ExecutionCommand getExecutionCommand() { 333 | return mExecutionCommand; 334 | } 335 | 336 | 337 | 338 | public interface AppShellClient { 339 | 340 | /** 341 | * Callback function for when {@link AppShell} exits. 342 | * 343 | * @param appShell The {@link AppShell} that exited. 344 | */ 345 | void onAppShellExited(AppShell appShell); 346 | 347 | } 348 | 349 | } 350 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 17 | 25 | 26 | 32 | 33 | 42 |