├── .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 | *
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
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 | *