Add ANSI Support

pull/155/head
Fox2Code 2 years ago
parent 78408ea689
commit d2e6a4917a

@ -9,6 +9,7 @@ Note: official repo do not accept new modules anymore, submit
Index:
- [Special notes](DEVELOPERS.md#special-notes)
- [Properties](DEVELOPERS.md#properties)
- [ANSI Styling](DEVELOPERS.md#ansi-styling)
- [Installer commands](DEVELOPERS.md#installer-commands)
## Special notes
@ -72,11 +73,19 @@ for some modules
Theses values are only used if not defined in the `module.prop` files
So the original module maker can still override them
## ANSI Styling
FoxMMM declare `ANSI_SUPPORT` to `true` if ANSI is supported.
It use [AndroidANSI](https://github.com/Fox2Code/AndroidANSI) library,
please check it's [README.md](https://github.com/Fox2Code/AndroidANSI/blob/master/README.md)
for the list of supported codes.
## Installer commands
The Fox's Mmm also allow better control over it's installer interface
FoxMmm also allow better control over it's installer interface
Fox's Mmm define the variable `MMM_EXT_SUPPORT` to expose it's extensions support
FoxMmm define the variable `MMM_EXT_SUPPORT` to expose it's extensions support
All the commands start with it `#!`, by default the manager process command as log output
unless `#!useExt` is sent to indicate that the app is ready to use commands
@ -97,6 +106,7 @@ Commands:
- `hideLoading`: Hide the indeterminate progress bar if previously shown
- `setSupportLink <url>`: Set support link to show when the install finish
(Note: Modules installed from repo will not show the config button if a link is set)
- `disableANSI`: Disable ANSI support if enabled
Variables:
- `MMM_EXT_SUPPORT` declared if extensions are supported
@ -131,10 +141,9 @@ mmm_exec hideLoading
mmm_exec setSupportLink https://github.com/Fox2Code/FoxMagiskModuleManager
```
You may look at the [example module](example_module) code or
download the [module zip](example_module.zip) and try it yourself
[You may look at the examples modules and their codes.](examples)
Have fun with the API making the user install experience a unique experience
Have fun with the API making your user install experience a unique experience
Also there is the source of the app icon
[here](https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=clipart&foreground.clipart=extension&foreground.space.trim=0&foreground.space.pad=0.25&foreColor=rgb(255%2C%20255%2C%20255)&backColor=rgb(255%2C%20152%2C%200)&crop=0&backgroundShape=circle&effects=elevate&name=ic_launcher)

@ -98,7 +98,8 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
implementation 'com.squareup.okhttp3:okhttp-brotli:4.9.3'
implementation 'com.github.topjohnwu.libsu:io:5.0.1'
implementation 'com.github.Fox2Code:RosettaX:46ec630055'
implementation 'com.github.Fox2Code:RosettaX:1.0.1'
implementation 'com.github.Fox2Code:AndroidANSI:1.0.1'
// Markdown
implementation "io.noties.markwon:core:4.6.2"

@ -21,10 +21,12 @@ import java.util.HashMap;
// See https://docs.github.com/en/rest/reference/repos#releases
public class AppUpdateManager {
public static int FLAG_COMPAT_LOW_QUALITY = 0x01;
public static int FLAG_COMPAT_NO_EXT = 0x02;
public static int FLAG_COMPAT_MAGISK_CMD = 0x04;
public static int FLAG_COMPAT_NEED_32BIT = 0x08;
public static int FLAG_COMPAT_MALWARE = 0x10;
public static int FLAG_COMPAT_NO_EXT = 0x02;
public static int FLAG_COMPAT_MAGISK_CMD = 0x04;
public static int FLAG_COMPAT_NEED_32BIT = 0x08;
public static int FLAG_COMPAT_MALWARE = 0x10;
public static int FLAG_COMPAT_NO_ANSI = 0x20;
public static int FLAG_COMPAT_FORCE_ANSI = 0x40;
private static final String TAG = "AppUpdateManager";
private static final AppUpdateManager INSTANCE = new AppUpdateManager();
private static final String RELEASES_API_URL =
@ -201,6 +203,12 @@ public class AppUpdateManager {
case "malware":
value |= FLAG_COMPAT_MALWARE;
break;
case "noANSI":
value |= FLAG_COMPAT_NO_ANSI;
break;
case "forceANSI":
value |= FLAG_COMPAT_FORCE_ANSI;
break;
}
}
compatDataId.put(line.substring(0, i), value);

@ -15,7 +15,8 @@ import android.widget.Toast;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.mmm.module.ActionButtonType;
import com.fox2code.androidansi.AnsiConstants;
import com.fox2code.androidansi.AnsiParser;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
@ -23,6 +24,7 @@ import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.module.ActionButtonType;
import com.fox2code.mmm.utils.FastException;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
@ -119,7 +121,8 @@ public class InstallerActivity extends CompatActivity {
this.progressIndicator = findViewById(R.id.progress_bar);
this.rebootFloatingButton = findViewById(R.id.install_terminal_reboot_fab);
this.installerTerminal = new InstallerTerminal(
installTerminal = findViewById(R.id.install_terminal), foreground);
installTerminal = findViewById(R.id.install_terminal),
this.isLightTheme(), foreground);
(horizontalScroller != null ? horizontalScroller : installTerminal)
.setBackground(new ColorDrawable(background));
this.progressIndicator.setVisibility(View.GONE);
@ -249,8 +252,26 @@ public class InstallerActivity extends CompatActivity {
"! Failed to extract test install script", "");
return;
}
this.installerTerminal.enableAnsi();
// Extract customize.sh manually in rootless mode because unzip might not exists
try (ZipFile zipFile = new ZipFile(file)) {
ZipEntry zipEntry = zipFile.getEntry("customize.sh");
if (zipEntry != null) {
try (FileOutputStream fileOutputStream = new FileOutputStream(
new File(file.getParentFile(), "customize.sh"))) {
Files.copy(zipFile.getInputStream(zipEntry), fileOutputStream);
}
}
} catch (Exception e) {
Log.d(TAG, "Failed ot extract install script via java code", e);
}
installerMonitor = new InstallerMonitor(installScript);
installJob = Shell.cmd("export MMM_EXT_SUPPORT=1",
"export MMM_USER_LANGUAGE=" + (MainApplication.isForceEnglish() ? "en-US" :
Resources.getSystem().getConfiguration().locale.toLanguageTag()),
"export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME,
"export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"),
AnsiConstants.ANSI_CMD_SUPPORT,
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"sh \"" + installScript.getAbsolutePath() + "\"" +
" 3 0 \"" + file.getAbsolutePath() + "\"")
@ -372,15 +393,25 @@ public class InstallerActivity extends CompatActivity {
installerMonitor = new InstallerMonitor(installExecutable);
if (moduleId != null) installerMonitor.setForCleanUp(moduleId);
if (noExtensions) {
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_FORCE_ANSI) != 0)
this.installerTerminal.enableAnsi();
else this.installerTerminal.disableAnsi();
installJob = Shell.cmd(arch32, "export BOOTMODE=true", // No Extensions
this.installerTerminal.isAnsiEnabled() ?
AnsiConstants.ANSI_CMD_SUPPORT : "true",
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
installCommand).to(installerController, installerMonitor);
} else {
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NO_ANSI) != 0)
this.installerTerminal.enableAnsi();
else this.installerTerminal.disableAnsi();
installJob = Shell.cmd(arch32, "export MMM_EXT_SUPPORT=1",
"export MMM_USER_LANGUAGE=" + (MainApplication.isForceEnglish() ? "en-US" :
Resources.getSystem().getConfiguration().locale.toLanguageTag()),
"export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME,
"export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"),
this.installerTerminal.isAnsiEnabled() ?
AnsiConstants.ANSI_CMD_SUPPORT : "true",
"export BOOTMODE=true", anyKernel3 ? "export AK3TMPFS=" +
InstallerInitializer.peekMagiskPath() + "/ak3tmpfs" :
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
@ -389,8 +420,7 @@ public class InstallerActivity extends CompatActivity {
}
boolean success = installJob.exec().isSuccess();
// Wait one UI cycle before disabling controller or processing results
UiThreadHandler.runAndWait(() -> {
}); // to avoid race conditions
UiThreadHandler.runAndWait(() -> {}); // to avoid race conditions
installerController.disable();
String message = "- Install successful";
if (!success) {
@ -433,6 +463,7 @@ public class InstallerActivity extends CompatActivity {
this.useExt = true;
return;
}
s = AnsiParser.patchEscapeSequence(s);
if (this.useExt && s.startsWith("#!")) {
this.processCommand(s.substring(2));
} else if (this.useRecovery && s.startsWith("progress ")) {
@ -464,7 +495,7 @@ public class InstallerActivity extends CompatActivity {
final String arg;
final String command;
int i = rawCommand.indexOf(' ');
if (i != -1) {
if (i != -1 && rawCommand.length() != i + 1) {
arg = rawCommand.substring(i + 1).trim();
command = rawCommand.substring(0, i);
} else {
@ -534,6 +565,9 @@ public class InstallerActivity extends CompatActivity {
arg.indexOf('/', 8) > 8))
this.supportLink = arg;
break;
case "disableANSI":
this.terminal.disableAnsi();
break;
}
}
@ -625,6 +659,7 @@ public class InstallerActivity extends CompatActivity {
@SuppressWarnings("SameParameterValue")
private void setInstallStateFinished(boolean success, String message, String optionalLink) {
this.installerTerminal.disableAnsi();
if (success && toDelete != null && !toDelete.delete()) {
SuFile suFile = new SuFile(toDelete.getAbsolutePath());
if (suFile.exists() && !suFile.delete())

@ -1,6 +1,7 @@
package com.fox2code.mmm.installer;
import android.graphics.Typeface;
import android.text.Spannable;
import android.view.ViewGroup;
import android.widget.TextView;
@ -8,20 +9,25 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.androidansi.AnsiContext;
import java.util.ArrayList;
public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.TextViewHolder> {
private final RecyclerView recyclerView;
private final ArrayList<String> terminal;
private final ArrayList<ProcessedLine> terminal;
private final AnsiContext ansiContext;
private final Object lock = new Object();
private final int foreground;
private boolean ansiEnabled = false;
public InstallerTerminal(RecyclerView recyclerView,int foreground) {
public InstallerTerminal(RecyclerView recyclerView, boolean isLightTheme,int foreground) {
recyclerView.setLayoutManager(
new LinearLayoutManager(recyclerView.getContext()));
this.recyclerView = recyclerView;
this.foreground = foreground;
this.terminal = new ArrayList<>();
this.ansiContext = (isLightTheme ? AnsiContext.LIGHT : AnsiContext.DARK).copy();
this.recyclerView.setAdapter(this);
}
@ -33,7 +39,7 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
@Override
public void onBindViewHolder(@NonNull TextViewHolder holder, int position) {
holder.setText(this.terminal.get(position));
this.terminal.get(position).setText(holder.textView);
}
@Override
@ -45,27 +51,20 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
synchronized (lock) {
boolean bottom = !this.recyclerView.canScrollVertically(1);
int index = this.terminal.size();
this.terminal.add(line);
this.terminal.add(this.process(line));
this.notifyItemInserted(index);
if (bottom) this.recyclerView.scrollToPosition(index);
}
}
public void setLine(int index, String line) {
synchronized (lock) {
this.terminal.set(index, line);
this.notifyItemChanged(index);
}
}
public void setLastLine(String line) {
synchronized (lock) {
int size = this.terminal.size();
if (size == 0) {
this.terminal.add(line);
this.terminal.add(this.process(line));
this.notifyItemInserted(0);
} else {
this.terminal.set(size - 1, line);
this.terminal.set(size - 1, this.process(line));
this.notifyItemChanged(size - 1);
}
}
@ -75,7 +74,7 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
synchronized (lock) {
int size = this.terminal.size();
return size == 0 ? "" :
this.terminal.get(size - 1);
this.terminal.get(size - 1).line;
}
}
@ -111,6 +110,25 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
}
}
public void enableAnsi() {
this.ansiEnabled = true;
}
public void disableAnsi() {
this.ansiEnabled = false;
this.ansiContext.reset();
}
public boolean isAnsiEnabled() {
return this.ansiEnabled;
}
private ProcessedLine process(String line) {
if (line.isEmpty()) return new ProcessedLine(" ", null);
return new ProcessedLine(line, this.ansiEnabled ?
this.ansiContext.parseAsSpannable(line) : null);
}
public static class TextViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
@ -128,4 +146,19 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
this.textView.setText(text.isEmpty() ? " " : text);
}
}
private static class ProcessedLine {
public final String line;
public final Spannable spannable;
ProcessedLine(String line, Spannable spannable) {
this.line = line;
this.spannable = spannable;
}
public void setText(TextView textView) {
textView.setText(this.spannable == null ?
this.line: this.spannable);
}
}
}

@ -31,7 +31,6 @@ import com.mikepenz.aboutlibraries.LibsBuilder;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.HashSet;
import java.util.Locale;
public class SettingsActivity extends CompatActivity {
private static int devModeStep = 0;

@ -13,6 +13,7 @@
android:layout_width="match_parent"
android:id="@+id/install_horizontal_scroller"
android:background="@color/black"
android:overScrollMode="never"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

@ -14,6 +14,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="16sp"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

Loading…
Cancel
Save