remove packagemanager
Some checks failed
Gralde check / build (push) Failing after 9s

This commit is contained in:
expvintl 2024-07-23 23:30:34 +08:00
parent 236072395c
commit 0e9d9dc0e8
37 changed files with 26 additions and 2524 deletions

View File

@ -134,21 +134,11 @@
android:exported="false"
android:label="@string/error"
android:theme="@style/AppTheme.NoActionBar.Dark"/>
<activity
android:name=".ui.other.SetupActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".ui.other.BonusActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/AppTheme.NoActionBar.Dark"/>
<activity
android:name=".ui.pm.PackageManagerActivity"
android:exported="false"
android:label="@string/package_settings"
android:theme="@style/AppTheme.NoActionBar.Dark"/>
<activity
android:name=".ui.customize.CustomizeActivity"
android:exported="false"

View File

@ -7,12 +7,11 @@
#include <fcntl.h>
static const char *rewrite_executable(const char *filename, char *buffer, int buffer_len) {
strcpy(buffer, "/data/data/io.neoterm/files/usr/bin/");
char *bin_match = strstr(filename, "/bin/");
if (bin_match == filename || bin_match == (filename + 4)) {
// We have either found "/bin/" at the start of the string or at
// "/xxx/bin/". Take the path after that.
strncpy(buffer + 36, bin_match + 5, (size_t) (buffer_len - 37));
strncpy(buffer, bin_match + 5, (size_t) (buffer_len - 1));
filename = buffer;
}
return filename;

View File

@ -108,7 +108,7 @@ static int create_subprocess(JNIEnv *env,
// Show terminal output about failing exec() call:
char *error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1)
error_message = const_cast<char *>("exec()");;
error_message = const_cast<char *>("exec()");
perror(error_message);
_exit(1);
}

View File

@ -7,7 +7,6 @@ import io.neoterm.component.completion.CompletionComponent
import io.neoterm.component.config.ConfigureComponent
import io.neoterm.component.extrakey.ExtraKeyComponent
import io.neoterm.component.font.FontComponent
import io.neoterm.component.pm.PackageComponent
import io.neoterm.component.profile.ProfileComponent
import io.neoterm.component.session.SessionComponent
import io.neoterm.component.session.ShellProfile
@ -80,7 +79,6 @@ object NeoInitializer {
ComponentManager.registerComponent(UserScriptComponent::class.java)
ComponentManager.registerComponent(ExtraKeyComponent::class.java)
ComponentManager.registerComponent(CompletionComponent::class.java)
ComponentManager.registerComponent(PackageComponent::class.java)
ComponentManager.registerComponent(SessionComponent::class.java)
ComponentManager.registerComponent(ProfileComponent::class.java)

View File

@ -84,18 +84,6 @@ object NeoPreference {
val dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f, context.resources.displayMetrics)
MIN_FONT_SIZE = (4f * dipInPixels).toInt()
MAX_FONT_SIZE = 256
// load apt source
val sourceFile = File(NeoTermPath.SOURCE_FILE)
kotlin.runCatching {
Files.readAllBytes(sourceFile.toPath())?.let {
val source = String(it).trim().trimEnd()
val array = source.split(" ")
if (array.size >= 2 && array[0] == "deb") {
store(R.string.key_package_source, array[1])
}
}
}
}
fun store(key: Int, value: Any) {
@ -156,7 +144,6 @@ object NeoPreference {
val loginProgramPath = findLoginProgram(loginProgramName) ?: return false
store(R.string.key_general_shell, loginProgramName)
symlinkLoginShell(loginProgramPath)
return true
}
@ -167,39 +154,17 @@ object NeoPreference {
fun getLoginShellPath(): String {
val loginProgramName = getLoginShellName()
// Some programs like ssh needs it
val shell = File(NeoTermPath.NEOTERM_LOGIN_SHELL_PATH)
val loginProgramPath = findLoginProgram(loginProgramName) ?: {
setLoginShellName(DefaultValues.loginShell)
"${NeoTermPath.USR_PATH}/bin/${DefaultValues.loginShell}"
loadString(R.string.key_general_shell,DefaultValues.loginShell)
}()
if (!shell.exists()) {
symlinkLoginShell(loginProgramPath)
}
return loginProgramPath
}
fun validateFontSize(fontSize: Int): Int {
return Math.max(MIN_FONT_SIZE, Math.min(fontSize, MAX_FONT_SIZE))
}
private fun symlinkLoginShell(loginProgramPath: String) {
File(NeoTermPath.CUSTOM_PATH).mkdirs()
try {
val shellSymlink = File(NeoTermPath.NEOTERM_LOGIN_SHELL_PATH)
if (shellSymlink.exists()) {
shellSymlink.delete()
}
Os.symlink(loginProgramPath, NeoTermPath.NEOTERM_LOGIN_SHELL_PATH)
Os.chmod(NeoTermPath.NEOTERM_LOGIN_SHELL_PATH, 448 /* Decimal of 0700 */)
} catch (e: ErrnoException) {
NLog.e("Preference", "Failed to symlink login shell: ${e.localizedMessage}")
e.printStackTrace()
}
}
fun findLoginProgram(loginProgramName: String): String? {
val file = File("${NeoTermPath.USR_PATH}/bin", loginProgramName)
return if (file.canExecute()) file.absolutePath else null

View File

@ -18,7 +18,7 @@ object DefaultValues {
const val enableSpecialVolumeKeys = false
const val enableWordBasedIme = false
const val loginShell = "bash"
const val loginShell = "sh"
const val initialCommand = ""
const val defaultFont = "SourceCodePro"
}
@ -28,7 +28,6 @@ object NeoTermPath {
const val ROOT_PATH = "/data/data/io.neoterm/files"
const val USR_PATH = "$ROOT_PATH/usr"
const val HOME_PATH = "$ROOT_PATH/home"
const val APT_BIN_PATH = "$USR_PATH/bin/apt"
const val LIB_PATH = "$USR_PATH/lib"
const val CUSTOM_PATH = "$HOME_PATH/.neoterm"
@ -40,14 +39,4 @@ object NeoTermPath {
const val USER_SCRIPT_PATH = "$CUSTOM_PATH/script"
const val PROFILE_PATH = "$CUSTOM_PATH/profile"
const val SOURCE_FILE = "$USR_PATH/etc/apt/sources.list"
const val PACKAGE_LIST_DIR = "$USR_PATH/var/lib/apt/lists"
private const val SOURCE = "https://raw.githubusercontent.com/NeoTerm/NeoTerm-repo/main"
val DEFAULT_MAIN_PACKAGE_SOURCE: String
init {
DEFAULT_MAIN_PACKAGE_SOURCE = SOURCE
}
}

View File

@ -1,170 +0,0 @@
package io.neoterm.component.pm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* @author kiva
*/
public class NeoPackageParser {
public interface ParseStateListener {
void onStartState();
void onEndState();
NeoPackageInfo onCreatePackageInfo();
void onStartParsePackage(String name, NeoPackageInfo packageInfo);
void onEndParsePackage(NeoPackageInfo packageInfo);
}
private static final String
KEY_PACKAGE_NAME = "Package",
KEY_VERSION = "Version",
KEY_ESSENTIAL = "Essential",
KEY_ARCH = "Architecture",
KEY_MAINTAINER = "Maintainer",
KEY_INSTALLED_SIZE = "Installed-Size",
KEY_DEPENDS = "Depends",
KEY_FILENAME = "Filename",
KEY_SIZE = "Size",
KEY_MD5 = "MD5sum",
KEY_SHA1 = "SHA1",
KEY_SHA256 = "SHA256",
KEY_HOMEPAGE = "Homepage",
KEY_DESC = "Description";
private BufferedReader reader;
private ParseStateListener stateListener;
NeoPackageParser(InputStream inputStream) {
reader = new BufferedReader(new InputStreamReader(inputStream));
}
void setStateListener(ParseStateListener stateListener) {
this.stateListener = stateListener;
}
public void parse() throws IOException {
if (stateListener == null) {
return;
}
String line;
String[] splits = new String[2];
String key = null;
String value = null;
boolean appendMode = false;
NeoPackageInfo packageInfo = null;
stateListener.onStartState();
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
if (splitKeyAndValue(line, splits)) {
key = splits[0];
value = splits[1];
appendMode = false;
} else {
if (key == null) {
// no key provided, we don't know where the value should be appended to
continue;
}
// the rest value to previous key
value = line.trim();
appendMode = true;
}
if (key.equals(KEY_PACKAGE_NAME)) {
if (packageInfo != null) {
stateListener.onEndParsePackage(packageInfo);
}
packageInfo = stateListener.onCreatePackageInfo();
packageInfo.setPackageName(value);
stateListener.onStartParsePackage(value, packageInfo);
}
if (packageInfo == null) {
continue;
}
if (appendMode) {
value = appendToLastValue(packageInfo, key, value);
}
switch (key) {
case KEY_ARCH:
packageInfo.setArchitecture(Architecture.Companion.parse(value));
break;
case KEY_DEPENDS:
packageInfo.setDependenciesString(value);
break;
case KEY_DESC:
packageInfo.setDescription(value);
break;
case KEY_ESSENTIAL:
packageInfo.setEssential(value.equals("yes"));
break;
case KEY_FILENAME:
packageInfo.setFileName(value);
break;
case KEY_HOMEPAGE:
packageInfo.setHomePage(value);
break;
case KEY_INSTALLED_SIZE:
packageInfo.setInstalledSizeInBytes(Long.parseLong(value));
break;
case KEY_MAINTAINER:
packageInfo.setMaintainer(value);
break;
case KEY_MD5:
packageInfo.setMd5(value);
break;
case KEY_SHA1:
packageInfo.setSha1(value);
break;
case KEY_SHA256:
packageInfo.setSha256(value);
break;
case KEY_SIZE:
packageInfo.setSizeInBytes(Long.parseLong(value));
break;
case KEY_VERSION:
packageInfo.setVersion(value);
break;
}
}
if (packageInfo != null) {
stateListener.onEndParsePackage(packageInfo);
}
stateListener.onEndState();
}
private String appendToLastValue(NeoPackageInfo packageInfo, String key, String value) {
// Currently, only descriptions can be multiline
switch (key) {
case KEY_DESC:
return packageInfo.getDescription() + " " + value;
default:
return value;
}
}
private boolean splitKeyAndValue(String line, String[] splits) {
int valueIndex = line.indexOf(':');
if (valueIndex < 0) {
return false;
}
splits[0] = line.substring(0, valueIndex).trim();
splits[1] = line.substring(valueIndex == line.length() ? valueIndex : valueIndex + 1).trim();
return true;
}
}

View File

@ -1,120 +0,0 @@
package io.neoterm.component.pm;
import io.neoterm.component.NeoComponent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
/**
* @author kiva
*/
public class PackageComponent implements NeoComponent {
private final Object lock = new Object();
private boolean isRefreshing = false;
private boolean queryEnabled = true;
private HashMap<String, NeoPackageInfo> neoPackages;
private NeoPackageInfo getPackageInfo(String packageName) {
return queryEnabled ? neoPackages.get(packageName) : null;
}
public HashMap<String, NeoPackageInfo> getPackages() {
return queryEnabled ? neoPackages : new HashMap<>();
}
public int getPackageCount() {
return queryEnabled ? neoPackages.size() : -1;
}
public SourceManager getSourceManager() {
return new SourceManager();
}
public void reloadPackages(File packageListFile, boolean clearPrevious) throws IOException {
synchronized (lock) {
if (isRefreshing) {
return;
}
isRefreshing = true;
}
tryParsePackages(packageListFile, clearPrevious);
synchronized (lock) {
isRefreshing = false;
}
}
public void clearPackages() {
if (isRefreshing) {
return;
}
neoPackages.clear();
}
private void tryParsePackages(File packageListFile, final boolean clearPrevious) throws IOException {
NeoPackageParser packageParser = new NeoPackageParser(new FileInputStream(packageListFile));
packageParser.setStateListener(new NeoPackageParser.ParseStateListener() {
@Override
public void onStartState() {
queryEnabled = false;
if (clearPrevious) {
neoPackages.clear();
}
}
@Override
public void onEndState() {
queryEnabled = true;
for (NeoPackageInfo info : neoPackages.values()) {
resolveDepends(info);
}
}
@Override
public NeoPackageInfo onCreatePackageInfo() {
return new NeoPackageInfo();
}
@Override
public void onStartParsePackage(String name, NeoPackageInfo packageInfo) {
}
@Override
public void onEndParsePackage(NeoPackageInfo packageInfo) {
neoPackages.put(packageInfo.getPackageName(), packageInfo);
}
});
packageParser.parse();
}
private void resolveDepends(NeoPackageInfo info) {
String dep = info.getDependenciesString();
if (dep == null) {
return;
}
String[] splits = dep.split(",");
NeoPackageInfo[] depends = new NeoPackageInfo[splits.length];
info.setDependencies(depends);
for (int i = 0; i < splits.length; ++i) {
String item = splits[i].trim();
depends[i] = getPackageInfo(item);
}
}
@Override
public void onServiceInit() {
neoPackages = new HashMap<>();
}
@Override
public void onServiceDestroy() {
}
@Override
public void onServiceObtained() {
}
}

View File

@ -1,29 +0,0 @@
package io.neoterm.component.pm;
import io.neoterm.framework.database.annotation.ID;
import io.neoterm.framework.database.annotation.Table;
/**
* @author kiva
*/
@Table
public class Source {
@ID(autoIncrement = true)
private int id;
public String url;
public String repo;
public boolean enabled;
public Source() {
// for Database
}
public Source(String url, String repo, boolean enabled) {
this.url = url;
this.repo = repo;
this.enabled = enabled;
}
}

View File

@ -1,35 +0,0 @@
package io.neoterm.component.pm
enum class Architecture {
ALL, ARM, AARCH64, X86, X86_64;
companion object {
fun parse(arch: String): Architecture {
return when (arch) {
"arm" -> ARM
"aarch64" -> AARCH64
"x86" -> X86
"x86_64" -> X86_64
else -> ALL
}
}
}
}
class NeoPackageInfo {
var packageName: String? = null
var isEssential: Boolean = false
var version: String? = null
var architecture: Architecture = Architecture.ALL
var maintainer: String? = null
var installedSizeInBytes: Long = 0L
var fileName: String? = null
var dependenciesString: String? = null
var dependencies: Array<NeoPackageInfo>? = null
var sizeInBytes: Long = 0L
var md5: String? = null
var sha1: String? = null
var sha256: String? = null
var homePage: String? = null
var description: String? = null
}

View File

@ -1,120 +0,0 @@
package io.neoterm.component.pm
import io.neoterm.App
import io.neoterm.R
import io.neoterm.component.ComponentManager
import io.neoterm.component.config.NeoTermPath
import io.neoterm.framework.NeoTermDatabase
import io.neoterm.utils.NLog
import java.io.File
import java.net.URL
import java.nio.file.Files
import java.nio.file.Paths
object SourceHelper {
fun syncSource() {
val sourceManager = ComponentManager.getComponent<PackageComponent>().sourceManager
syncSource(sourceManager)
}
fun syncSource(sourceManager: SourceManager) {
val content = buildString {
this.append("# Generated by NeoTerm-Preference\n")
sourceManager.getEnabledSources()
.joinTo(this, "\n") { "deb [trusted=yes] ${it.url} ${it.repo}\n" }
}
kotlin.runCatching {
Files.write(Paths.get(NeoTermPath.SOURCE_FILE), content.toByteArray())
}
}
fun detectSourceFiles(): List<File> {
val sourceManager = ComponentManager.getComponent<PackageComponent>().sourceManager
val sourceFiles = ArrayList<File>()
try {
val prefixes = sourceManager.getEnabledSources()
.map { detectSourceFilePrefix(it) }
.filter { it.isNotEmpty() }
File(NeoTermPath.PACKAGE_LIST_DIR)
.listFiles()
.filterTo(sourceFiles) { file ->
prefixes.filter { file.name.startsWith(it) }
.count() > 0
}
} catch (e: Exception) {
sourceFiles.clear()
NLog.e("PM", "Failed to detect source files: ${e.localizedMessage}")
}
return sourceFiles
}
fun detectSourceFilePrefix(source: Source): String {
try {
val url = URL(source.url)
val builder = StringBuilder(url.host)
if (url.port != -1) {
builder.append(":${url.port}")
}
val path = url.path
if (path != null && path.isNotEmpty()) {
builder.append("_")
val fixedPath = path.replace("/", "_").substring(1) // skip the last '/'
builder.append(fixedPath)
}
builder.append("_dists_${source.repo.replace(" ".toRegex(), "_")}_binary-")
return builder.toString()
} catch (e: Exception) {
NLog.e("PM", "Failed to detect source file prefix: ${e.localizedMessage}")
return ""
}
}
}
class SourceManager internal constructor() {
private val database = NeoTermDatabase.instance("sources")
init {
if (database.findAll<Source>(Source::class.java).isEmpty()) {
App.get().resources.getStringArray(R.array.pref_package_source_values)
.forEach {
database.saveBean(Source(it, "stable main", true))
}
}
}
fun addSource(sourceUrl: String, repo: String, enabled: Boolean) {
database.saveBean(Source(sourceUrl, repo, enabled))
}
fun removeSource(sourceUrl: String) {
database.deleteBeanByWhere(Source::class.java, "url == '$sourceUrl'")
}
fun updateAll(sources: List<Source>) {
database.dropAllTable()
database.saveBeans(sources)
}
fun getAllSources(): List<Source> {
return database.findAll(Source::class.java)
}
fun getEnabledSources(): List<Source> {
return getAllSources().filter { it.enabled }
}
fun getMainPackageSource(): String {
return getEnabledSources()
.map { it.repo }
.singleOrNull { it.trim() == "stable main" }
?: NeoTermPath.DEFAULT_MAIN_PACKAGE_SOURCE
}
fun applyChanges() {
database.vacuum()
}
}

View File

@ -94,7 +94,6 @@ class SessionComponent : NeoComponent {
.executablePath(parameter.executablePath)
.currentWorkingDirectory(parameter.cwd)
.callback(parameter.sessionCallback)
.systemShell(parameter.systemShell)
.envArray(parameter.env)
.argArray(parameter.arguments)
.initialCommand(parameter.initialCommand)

View File

@ -27,7 +27,6 @@ class ShellParameter {
var initialCommand: String? = null
var env: Array<Pair<String, String>>? = null
var sessionCallback: TerminalSession.SessionChangedCallback? = null
var systemShell: Boolean = false
var shellProfile: ShellProfile? = null
fun executablePath(executablePath: String?): ShellParameter {
@ -60,11 +59,6 @@ class ShellParameter {
return this
}
fun systemShell(systemShell: Boolean): ShellParameter {
this.systemShell = systemShell
return this
}
fun profile(shellProfile: ShellProfile): ShellParameter {
this.shellProfile = shellProfile
return this
@ -291,21 +285,14 @@ open class ShellTermSession private constructor(
return this
}
fun systemShell(systemShell: Boolean): Builder {
this.systemShell = systemShell
return this
}
fun create(context: Context): ShellTermSession {
val cwd = this.cwd ?: NeoTermPath.HOME_PATH
val shell = this.executablePath ?: if (systemShell)
"/system/bin/sh"
else
shellProfile.loginShell
val shell = shellProfile.loginShell
val args = this.args ?: mutableListOf(shell)
val env = transformEnvironment(this.env) ?: buildEnvironment(cwd, systemShell)
val env = transformEnvironment(this.env) ?: buildEnvironment(cwd)
val callback = changeCallback ?: TermSessionCallback()
return ShellTermSession(
shell, cwd, args.toTypedArray(), env, callback,
@ -324,7 +311,7 @@ open class ShellTermSession private constructor(
}
private fun buildEnvironment(cwd: String?, systemShell: Boolean): Array<String> {
private fun buildEnvironment(cwd: String?): Array<String> {
val selectedCwd = cwd ?: NeoTermPath.HOME_PATH
File(NeoTermPath.HOME_PATH).mkdirs()
@ -336,62 +323,11 @@ open class ShellTermSession private constructor(
val externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE")
val colorterm = "COLORTERM=truecolor"
// PY Trade: Some programs support NeoTerm in a special way.
val neotermIdEnv = "__NEOTERM=1"
val originPathEnv = "__NEOTERM_ORIGIN_PATH=" + buildOriginPathEnv()
val originLdEnv = "__NEOTERM_ORIGIN_LD_LIBRARY_PATH=" + buildOriginLdLibEnv()
return if (systemShell) {
val pathEnv = "PATH=" + System.getenv("PATH")
arrayOf(
return arrayOf(
termEnv, homeEnv, androidRootEnv, androidDataEnv,
externalStorageEnv, pathEnv, neotermIdEnv, prefixEnv,
originLdEnv, originPathEnv, colorterm
)
} else {
val ps1Env = "PS1=$ "
val langEnv = "LANG=en_US.UTF-8"
val pathEnv = "PATH=" + buildPathEnv()
val ldEnv = "LD_LIBRARY_PATH=" + buildLdLibraryEnv()
val pwdEnv = "PWD=$selectedCwd"
val tmpdirEnv = "TMPDIR=${NeoTermPath.USR_PATH}/tmp"
// execve(2) wrapper to avoid incorrect shebang
val ldPreloadEnv = if (shellProfile.enableExecveWrapper) {
"LD_PRELOAD=${App.get().applicationInfo.nativeLibraryDir}/libnexec.so"
} else {
""
}
arrayOf(
termEnv, homeEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv,
androidRootEnv, androidDataEnv, externalStorageEnv,
tmpdirEnv, neotermIdEnv, originPathEnv, originLdEnv,
ldPreloadEnv, prefixEnv, colorterm
)
}
.filter { it.isNotEmpty() }
.toTypedArray()
}
private fun buildOriginPathEnv(): String {
val path = System.getenv("PATH")
return path ?: ""
}
private fun buildOriginLdLibEnv(): String {
val path = System.getenv("LD_LIBRARY_PATH")
return path ?: ""
}
private fun buildLdLibraryEnv(): String {
return "${NeoTermPath.USR_PATH}/lib"
}
private fun buildPathEnv(): String {
return "${NeoTermPath.USR_PATH}/bin:${NeoTermPath.USR_PATH}/bin/applets"
externalStorageEnv, pathEnv, prefixEnv, colorterm
).filter { it.isNotEmpty() }.toTypedArray()
}
}
}

View File

@ -53,7 +53,6 @@ class TerminalDialog(val context: Context) {
.executablePath(executablePath)
.arguments(arguments)
.callback(terminalSessionCallback)
.systemShell(false)
terminalSession = Terminals.createSession(context, parameter)
if (terminalSession is ShellTermSession) {
(terminalSession as ShellTermSession).exitPrompt = context.getString(R.string.process_exit_prompt_press_back)

View File

@ -1,160 +0,0 @@
package io.neoterm.setup;
import android.app.ProgressDialog;
import android.system.Os;
import android.util.Pair;
import androidx.appcompat.app.AppCompatActivity;
import io.neoterm.backend.EmulatorDebug;
import io.neoterm.component.config.NeoTermPath;
import io.neoterm.utils.NLog;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* @author kiva
*/
final class SetupThread extends Thread {
private final SourceConnection sourceConnection;
private final File prefixPath;
private final AppCompatActivity activity;
private final ResultListener resultListener;
private final ProgressDialog progressDialog;
public SetupThread(AppCompatActivity activity, SourceConnection sourceConnection,
File prefixPath, ResultListener resultListener,
ProgressDialog progressDialog) {
this.activity = activity;
this.sourceConnection = sourceConnection;
this.prefixPath = prefixPath;
this.resultListener = resultListener;
this.progressDialog = progressDialog;
}
@Override
public void run() {
try {
final String stagingPrefixPath = NeoTermPath.ROOT_PATH + "/usr-staging";
final File stagingPrefixFile = new File(stagingPrefixPath);
if (stagingPrefixFile.exists()) {
deleteFolder(stagingPrefixFile);
}
int totalReadBytes = 0;
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
try (ZipInputStream zipInput = new ZipInputStream(sourceConnection.getInputStream())) {
ZipEntry zipEntry;
int totalBytes = sourceConnection.getSize();
while ((zipEntry = zipInput.getNextEntry()) != null) {
totalReadBytes += zipEntry.getCompressedSize();
final int totalReadBytesFinal = totalReadBytes;
final int totalBytesFinal = totalBytes;
activity.runOnUiThread(() -> {
try {
double progressFloat = ((double) totalReadBytesFinal) / ((double) totalBytesFinal) * 100.0;
progressDialog.setProgress((int) progressFloat);
} catch (RuntimeException ignore) {
// activity dismissed
}
});
if (zipEntry.getName().contains("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line;
while ((line = symlinksReader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
String[] parts = line.split("");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = stagingPrefixPath + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(stagingPrefixPath, zipEntryName);
if (zipEntry.isDirectory()) {
if (!targetFile.mkdirs())
throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
} else {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1) {
outStream.write(buffer, 0, readBytes);
}
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
}
}
}
}
sourceConnection.close();
if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
NLog.INSTANCE.e("Setup", "Linking " + symlink.first + " to " + symlink.second);
Os.symlink(symlink.first, symlink.second);
}
if (!stagingPrefixFile.renameTo(prefixPath)) {
throw new RuntimeException("Unable to rename staging folder");
}
activity.runOnUiThread(() -> resultListener.onResult(null));
} catch (final Exception e) {
NLog.INSTANCE.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(() -> {
try {
resultListener.onResult(e);
} catch (RuntimeException e1) {
// Activity already dismissed - ignore.
}
});
} finally {
activity.runOnUiThread(() -> {
try {
progressDialog.dismiss();
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
});
}
}
private static void deleteFolder(File fileOrDirectory) throws IOException {
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
File[] children = fileOrDirectory.listFiles();
if (children != null) {
for (File child : children) {
deleteFolder(child);
}
}
}
if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete "
+ (fileOrDirectory.isDirectory() ? "directory " : "file ")
+ fileOrDirectory.getAbsolutePath());
}
}
}

View File

@ -1,13 +0,0 @@
package io.neoterm.setup;
import java.io.IOException;
import java.io.InputStream;
/**
* @author kiva
*/
public interface SourceConnection {
InputStream getInputStream() throws IOException;
int getSize();
void close();
}

View File

@ -1,113 +0,0 @@
package io.neoterm.setup
import android.content.Context
import android.net.Uri
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
/**
* @author kiva
*/
class BackupFileConnection(context: Context, uri: Uri) : LocalFileConnection(context, uri)
/**
* @author kiva
*/
open class LocalFileConnection(context: Context, uri: Uri) : OfflineUriConnection(context, uri)
/**
* @author kiva
*/
class NetworkConnection(private val sourceUrl: String) : SourceConnection {
private var connection: HttpURLConnection? = null
@Throws(IOException::class)
override fun getInputStream(): InputStream {
if (connection == null) {
connection = openHttpConnection()
connection!!.connectTimeout = 8000
connection!!.readTimeout = 8000
}
return connection!!.inputStream
}
override fun getSize(): Int {
return if (connection != null) {
connection!!.contentLength
} else 0
}
override fun close() {
if (connection != null) {
connection!!.disconnect()
}
}
@Throws(IOException::class)
private fun openHttpConnection(): HttpURLConnection {
val arch = SetupHelper.determineArchName()
return URL("$sourceUrl/boot/$arch.zip").openConnection() as HttpURLConnection
}
}
/**
* @author kiva
*/
abstract class OfflineConnection : SourceConnection {
private var inputStream: InputStream? = null
@Throws(IOException::class)
protected abstract fun openInputStream(): InputStream
@Throws(IOException::class)
override fun getInputStream(): InputStream {
if (inputStream == null) {
inputStream = openInputStream()
}
return inputStream!!
}
override fun getSize(): Int {
if (inputStream != null) {
return try {
inputStream!!.available()
} catch (e: IOException) {
e.printStackTrace()
0
}
}
return 0
}
override fun close() {
if (inputStream != null) {
try {
inputStream!!.close()
} catch (ignore: IOException) {
ignore.printStackTrace()
}
}
}
}
/**
* @author kiva
*/
open class OfflineUriConnection(private val context: Context, private val uri: Uri) : OfflineConnection() {
@Throws(IOException::class)
override fun openInputStream(): InputStream {
return context.contentResolver.openInputStream(uri)
}
}

View File

@ -1,87 +0,0 @@
package io.neoterm.setup
import android.app.ProgressDialog
import android.content.Context
import android.os.Build
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import io.neoterm.App
import io.neoterm.R
import io.neoterm.component.config.NeoTermPath
import java.io.File
import java.util.*
/**
* @author kiva
*/
interface ResultListener {
fun onResult(error: Exception?)
}
/**
* @author kiva
*/
object SetupHelper {
fun needSetup(): Boolean {
val PREFIX_FILE = File(NeoTermPath.USR_PATH)
return !PREFIX_FILE.isDirectory
}
fun setup(
activity: AppCompatActivity, connection: SourceConnection,
resultListener: ResultListener
) {
if (!needSetup()) {
resultListener.onResult(null)
return
}
val prefixFile = File(NeoTermPath.USR_PATH)
val progress = makeProgressDialog(activity)
progress.max = 100
progress.show()
SetupThread(activity, connection, prefixFile, resultListener, progress).start()
}
private fun makeProgressDialog(context: Context): ProgressDialog {
return makeProgressDialog(context, context.getString(R.string.installer_message))
}
fun makeProgressDialog(context: Context, message: String): ProgressDialog {
val dialog = ProgressDialog(context)
dialog.setMessage(message)
dialog.isIndeterminate = false
dialog.setCancelable(false)
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
return dialog
}
fun makeErrorDialog(context: Context, messageId: Int): AlertDialog {
return makeErrorDialog(context, context.getString(messageId))
}
fun makeErrorDialog(context: Context, message: String): AlertDialog {
return AlertDialog.Builder(context)
.setTitle(R.string.error)
.setMessage(message)
.setPositiveButton(android.R.string.yes, null)
.setNeutralButton(R.string.show_help) { _, _ -> App.get().openHelpLink() }
.create()
}
fun determineArchName(): String {
for (androidArch in Build.SUPPORTED_ABIS) {
when (androidArch) {
"arm64-v8a" -> return "aarch64"
"armeabi-v7a" -> return "arm"
"x86_64" -> return "x86_64"
}
}
throw RuntimeException(
"Unable to determine arch from Build.SUPPORTED_ABIS = "
+ Arrays.toString(Build.SUPPORTED_ABIS)
)
}
}

View File

@ -44,7 +44,6 @@ open class BaseCustomizeActivity : AppCompatActivity() {
.executablePath("${NeoTermPath.USR_PATH}/bin/echo")
.arguments(arrayOf("echo", "-e", *script))
.callback(sessionCallback)
.systemShell(false)
session = Terminals.createSession(this, parameter)
terminalView.attachSession(session)

View File

@ -131,20 +131,6 @@ class AboutActivity : AppCompatActivity() {
findViewById<View>(R.id.about_source_code_view).setOnClickListener {
openUrl("https://github.com/NeoTerm/NeoTerm")
}
findViewById<View>(R.id.about_reset_app_view).setOnClickListener {
AlertDialog.Builder(this)
.setMessage(R.string.reset_app_warning)
.setPositiveButton(R.string.yes) { _, _ ->
resetApp()
}
.setNegativeButton(android.R.string.no, null)
.show()
}
}
private fun resetApp() {
startActivity(Intent(this, SetupActivity::class.java))
}
private fun openUrl(url: String) {

View File

@ -1,238 +0,0 @@
package io.neoterm.ui.other
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import io.neoterm.App
import io.neoterm.R
import io.neoterm.component.config.NeoTermPath
import io.neoterm.component.pm.SourceHelper
import io.neoterm.setup.*
import io.neoterm.utils.getPathOfMediaUri
import io.neoterm.utils.runApt
import java.io.File
/**
* @author kiva
*/
class SetupActivity : AppCompatActivity(), View.OnClickListener, ResultListener {
companion object {
private const val REQUEST_SELECT_PARAMETER = 520;
}
private var setupParameter = ""
private var setupParameterUri: Uri? = null
private val hintMapping = arrayOf(
R.id.setup_method_online, R.string.setup_hint_online,
R.id.setup_method_local, R.string.setup_hint_local,
R.id.setup_method_backup, R.string.setup_hint_backup
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ui_setup)
val parameterEditor = findViewById<EditText>(R.id.setup_source_parameter)
val tipText = findViewById<TextView>(R.id.setup_url_tip_text)
val onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { button, checked ->
if (checked) {
val id = button.id
val index = hintMapping.indexOf(id)
if (index < 0 || index % 2 != 0) {
parameterEditor.setHint(R.string.setup_input_source_parameter)
return@OnCheckedChangeListener
}
parameterEditor.setHint(hintMapping[index + 1])
tipText.setText(hintMapping[index + 1])
setDefaultValue(parameterEditor, id)
}
}
findViewById<RadioButton>(R.id.setup_method_online).setOnCheckedChangeListener(onCheckedChangeListener)
findViewById<RadioButton>(R.id.setup_method_local).setOnCheckedChangeListener(onCheckedChangeListener)
findViewById<RadioButton>(R.id.setup_method_backup).setOnCheckedChangeListener(onCheckedChangeListener)
findViewById<Button>(R.id.setup_next).setOnClickListener(this)
findViewById<Button>(R.id.setup_source_parameter_select).setOnClickListener(this)
}
override fun onClick(view: View?) {
val clickedId = view?.id ?: return
when (clickedId) {
R.id.setup_source_parameter_select -> doSelectParameter()
R.id.setup_next -> doPrepareSetup()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == REQUEST_SELECT_PARAMETER && resultCode == RESULT_OK) {
if (resultData != null) {
val path = this.getPathOfMediaUri(resultData.data)
findViewById<EditText>(R.id.setup_source_parameter).setText(path)
return
}
}
super.onActivityResult(requestCode, resultCode, resultData)
}
private fun doPrepareSetup() {
val id = findViewById<RadioGroup>(R.id.setup_method_group).checkedRadioButtonId
val editor = findViewById<EditText>(R.id.setup_source_parameter)
setupParameter = editor.text.toString()
if (setupParameterUri == null) {
when (id) {
R.id.setup_method_backup,
R.id.setup_method_local -> {
SetupHelper.makeErrorDialog(this, R.string.setup_error_parameter_null).show()
return
}
}
}
val dialog = SetupHelper.makeProgressDialog(this, getString(R.string.setup_preparing))
dialog.show()
Thread {
val errorMessage = validateParameter(id, setupParameter)
runOnUiThread {
dialog.dismiss()
editor.error = errorMessage
if (errorMessage != null) {
SetupHelper.makeErrorDialog(this, errorMessage).show()
return@runOnUiThread
}
val connection = createSourceConnection(id, setupParameter, setupParameterUri)
showConfirmDialog(connection)
}
}.start()
}
private fun doSelectParameter() {
val id = findViewById<RadioGroup>(R.id.setup_method_group).checkedRadioButtonId
when (id) {
R.id.setup_method_backup,
R.id.setup_method_local -> {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
try {
startActivityForResult(
Intent.createChooser(intent, getString(R.string.setup_local)),
REQUEST_SELECT_PARAMETER
)
} catch (ignore: ActivityNotFoundException) {
Toast.makeText(this, R.string.no_file_picker, Toast.LENGTH_SHORT).show()
}
}
R.id.setup_method_online -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null, false)
view.findViewById<TextView>(R.id.dialog_edit_text_info).text = getString(R.string.input_new_source_url)
val edit = view.findViewById<EditText>(R.id.dialog_edit_text_editor)
AlertDialog.Builder(this)
.setTitle(R.string.new_source)
.setView(view)
.setPositiveButton(android.R.string.yes) { _, _ ->
val newURL = edit.text.toString()
val parameterEditor = findViewById<EditText>(R.id.setup_source_parameter)
parameterEditor.setText(newURL)
}
.setNegativeButton(android.R.string.no, null)
.show()
}
}
}
private fun createSourceConnection(id: Int, parameter: String, parameterUri: Uri?): SourceConnection {
return when (id) {
R.id.setup_method_local -> LocalFileConnection(this, parameterUri!!)
R.id.setup_method_online -> NetworkConnection(parameter)
R.id.setup_method_backup -> BackupFileConnection(this, parameterUri!!)
else -> throw IllegalArgumentException("Unexpected setup method!")
}
}
private fun validateParameter(id: Int, parameter: String): String? {
return when (id) {
R.id.setup_method_online -> try {
java.net.URI.create(parameter)
null
} catch (e: IllegalArgumentException) {
getString(R.string.setup_error_invalid_url)
}
R.id.setup_method_local,
R.id.setup_method_backup -> if (File(parameter).exists()) null else getString(R.string.setup_error_file_not_found)
else -> null
}
}
private fun setDefaultValue(parameterEditor: EditText, id: Int) {
setupParameter = when (id) {
R.id.setup_method_online -> NeoTermPath.DEFAULT_MAIN_PACKAGE_SOURCE
else -> ""
}
parameterEditor.setText(setupParameter)
}
private fun showConfirmDialog(connection: SourceConnection) {
val needSetup = SetupHelper.needSetup()
val titleId = if (needSetup) R.string.setup_confirm else R.string.setup_reset_confirm
val messageId = if (needSetup) R.string.setup_confirm_text else R.string.setup_reset_confirm_text
AlertDialog.Builder(this)
.setTitle(titleId)
.setMessage(messageId)
.setPositiveButton(android.R.string.yes) { _, _ ->
doSetup(connection)
}
.setNegativeButton(android.R.string.no, null)
.show()
}
private fun doSetup(connection: SourceConnection) {
SetupHelper.setup(this, connection, this)
}
override fun onResult(error: Exception?) {
if (error == null) {
setResult(RESULT_OK)
SourceHelper.syncSource()
executeAptUpdate()
} else {
AlertDialog.Builder(this)
.setTitle(R.string.error)
.setMessage(error.toString())
.setNegativeButton(R.string.use_system_shell) { _, _ ->
setResult(RESULT_CANCELED)
finish()
}
.setNeutralButton(R.string.show_help) { _, _ ->
App.get().openHelpLink()
}
.setPositiveButton(android.R.string.yes, null)
.show()
}
}
private fun executeAptUpdate() = runApt("update") {
it.onSuccess { executeAptUpgrade() }
}
private fun executeAptUpgrade() = runApt("upgrade", "-y") {
it.onSuccess { finish() }
}
}

View File

@ -1,226 +0,0 @@
package io.neoterm.ui.pm
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuItemCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter
import io.neoterm.R
import io.neoterm.component.ComponentManager
import io.neoterm.component.config.NeoPreference
import io.neoterm.component.pm.*
import io.neoterm.utils.StringDistance
import io.neoterm.utils.runApt
import java.util.*
/**
* @author kiva
*/
class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListener, SortedListAdapter.Callback {
private val comparator = SortedListAdapter.ComparatorBuilder<PackageModel>()
.setOrderForModel<PackageModel>(PackageModel::class.java) { a, b ->
a.packageInfo.packageName!!.compareTo(b.packageInfo.packageName!!)
}
.build()
lateinit var recyclerView: androidx.recyclerview.widget.RecyclerView
lateinit var adapter: PackageAdapter
var models = listOf<PackageModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ui_pm_single_tab)
val toolbar = findViewById<Toolbar>(R.id.pm_toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
recyclerView = findViewById(R.id.pm_package_list)
recyclerView.setHasFixedSize(true)
adapter = PackageAdapter(this, comparator, object : PackageAdapter.Listener {
override fun onModelClicked(model: PackageModel) {
AlertDialog.Builder(this@PackageManagerActivity)
.setTitle(model.packageInfo.packageName)
.setMessage(model.getPackageDetails(this@PackageManagerActivity))
.setPositiveButton(R.string.install) { _, _ ->
installPackage(model.packageInfo.packageName)
}
.setNegativeButton(android.R.string.no, null)
.show()
}
})
adapter.addCallback(this)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
refreshPackageList()
}
private fun installPackage(packageName: String?) = packageName?.let {
runApt("install", "-y", it, autoClose = false) {
it.onSuccess { it.setTitle(getString(R.string.done)) }
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_pm, menu)
val searchItem = menu!!.findItem(R.id.action_search)
val searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView.setOnQueryTextListener(this)
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> finish()
R.id.action_source -> changeSource()
R.id.action_update_and_refresh -> executeAptUpdate()
R.id.action_refresh -> refreshPackageList()
R.id.action_upgrade -> executeAptUpgrade()
}
return super.onOptionsItemSelected(item)
}
private fun changeSource() {
val sourceManager = ComponentManager.getComponent<PackageComponent>().sourceManager
val sourceList = sourceManager.getAllSources()
val items = sourceList.map { "${it.url} :: ${it.repo}" }.toTypedArray()
val selection = sourceList.map { it.enabled }.toBooleanArray()
AlertDialog.Builder(this)
.setTitle(R.string.pref_package_source)
.setMultiChoiceItems(items, selection) { _, which, isChecked ->
sourceList[which].enabled = isChecked
}
.setPositiveButton(android.R.string.yes) { _, _ -> changeSourceInternal(sourceManager, sourceList) }
.setNeutralButton(R.string.new_source) { _, _ -> changeSourceToUserInput(sourceManager) }
.setNegativeButton(android.R.string.no, null)
.show()
}
@SuppressLint("SetTextI18n")
private fun changeSourceToUserInput(sourceManager: SourceManager) {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_two_text, null, false)
view.findViewById<TextView>(R.id.dialog_edit_text_info).text = getString(R.string.input_new_source_url)
view.findViewById<TextView>(R.id.dialog_edit_text2_info).text = getString(R.string.input_new_source_repo)
val urlEditor = view.findViewById<EditText>(R.id.dialog_edit_text_editor)
val repoEditor = view.findViewById<EditText>(R.id.dialog_edit_text2_editor)
repoEditor.setText("stable main")
AlertDialog.Builder(this)
.setTitle(R.string.pref_package_source)
.setView(view)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes) { _, _ ->
val url = urlEditor.text.toString()
val repo = repoEditor.text.toString()
var errored = false
if (url.trim().isEmpty()) {
urlEditor.error = getString(R.string.error_new_source_url)
errored = true
}
if (repo.trim().isEmpty()) {
repoEditor.error = getString(R.string.error_new_source_repo)
errored = true
}
if (errored) {
return@setPositiveButton
}
val source = urlEditor.text.toString()
sourceManager.addSource(source, repo, true)
postChangeSource(sourceManager)
}
.show()
}
private fun changeSourceInternal(sourceManager: SourceManager, source: List<Source>) {
sourceManager.updateAll(source)
postChangeSource(sourceManager)
}
private fun postChangeSource(sourceManager: SourceManager) {
sourceManager.applyChanges()
NeoPreference.store(R.string.key_package_source, sourceManager.getMainPackageSource())
SourceHelper.syncSource(sourceManager)
executeAptUpdate()
}
private fun executeAptUpdate() = runApt("update") {
it.onSuccess { refreshPackageList() }
}
private fun executeAptUpgrade() = runApt("update") { update ->
update.onSuccess {
runApt("upgrade", "-y") {
it.onSuccess { Toast.makeText(this, R.string.apt_upgrade_ok, Toast.LENGTH_SHORT).show() }
}
}
}
private fun refreshPackageList() = Thread {
val pm = ComponentManager.getComponent<PackageComponent>()
val sourceFiles = SourceHelper.detectSourceFiles()
pm.clearPackages()
sourceFiles.forEach { pm.reloadPackages(it, false) }
models = pm.packages.values.map { PackageModel(it) }.toList()
this@PackageManagerActivity.runOnUiThread {
adapter.edit().replaceAll(models).commit()
if (models.isEmpty()) {
Toast.makeText(this@PackageManagerActivity, R.string.package_list_empty, Toast.LENGTH_SHORT).show()
}
}
}.start()
private fun sortDistance(
models: List<PackageModel>, query: String,
mapper: (NeoPackageInfo) -> String
): List<Pair<PackageModel, Int>> {
return models
.map {
it to StringDistance.distance(mapper(it.packageInfo).toLowerCase(Locale.ROOT), query.toLowerCase(Locale.ROOT))
}
.sortedWith { l, r -> r.second.compareTo(l.second) }
.toList()
}
private fun filter(models: List<PackageModel>, query: String): List<PackageModel> {
val prepared = models.filter {
it.packageInfo.packageName!!.contains(query, true)
|| it.packageInfo.description!!.contains(query, true)
}
return sortDistance(prepared, query) { it.packageName!! }
.plus(sortDistance(prepared, query) { it.description!! })
.map { it.first }
.toList()
}
override fun onQueryTextSubmit(text: String?) = false
override fun onQueryTextChange(text: String?): Boolean {
text?.let { adapter.edit().replaceAll(filter(models, it)).commit() }
return true
}
override fun onEditStarted() {
recyclerView.animate().alpha(0.5f)
}
override fun onEditFinished() {
recyclerView.scrollToPosition(0)
recyclerView.animate().alpha(1.0f)
}
}

View File

@ -1,76 +0,0 @@
package io.neoterm.ui.pm
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import io.neoterm.R
import io.neoterm.component.pm.NeoPackageInfo
import io.neoterm.utils.formatSizeInKB
class PackageAdapter(
context: Context,
comparator: Comparator<PackageModel>,
private val listener: PackageAdapter.Listener
) : SortedListAdapter<PackageModel>(context, PackageModel::class.java, comparator),
FastScrollRecyclerView.SectionedAdapter {
override fun getSectionName(position: Int): String {
return getItem(position).packageInfo.packageName?.substring(0, 1) ?: "#"
}
interface Listener {
fun onModelClicked(model: PackageModel)
}
override fun onCreateViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
viewType: Int
): ViewHolder<out PackageModel> {
val rootView = inflater.inflate(R.layout.item_package, parent, false)
return PackageViewHolder(rootView, listener)
}
}
class PackageViewHolder(private val rootView: View, private val listener: PackageAdapter.Listener) :
SortedListAdapter.ViewHolder<PackageModel>(rootView) {
private val packageNameView: TextView = rootView.findViewById(R.id.package_item_name)
private val packageDescView: TextView = rootView.findViewById(R.id.package_item_desc)
override fun performBind(item: PackageModel) {
rootView.setOnClickListener { listener.onModelClicked(item) }
packageNameView.text = item.packageInfo.packageName
packageDescView.text = item.packageInfo.description
}
}
/**
* @author kiva
*/
class PackageModel(val packageInfo: NeoPackageInfo) : SortedListAdapter.ViewModel {
override fun <T> isSameModelAs(t: T): Boolean {
if (t is PackageModel) {
return t.packageInfo.packageName == packageInfo.packageName
}
return false
}
override fun <T> isContentTheSameAs(t: T): Boolean {
return isSameModelAs(t)
}
fun getPackageDetails(context: Context): String {
return context.getString(
R.string.package_details,
packageInfo.packageName, packageInfo.version,
packageInfo.dependenciesString,
packageInfo.installedSizeInBytes.formatSizeInKB(),
packageInfo.description, packageInfo.homePage
)
}
}

View File

@ -1,625 +0,0 @@
/**
* Copyright (C) 2015 nshmura
* Copyright (C) 2015 The Android Open Source Project
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.neoterm.ui.pm.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import io.neoterm.R;
public class RecyclerTabLayout extends RecyclerView {
protected static final long DEFAULT_SCROLL_DURATION = 200;
protected static final float DEFAULT_POSITION_THRESHOLD = 0.6f;
protected static final float POSITION_THRESHOLD_ALLOWABLE = 0.001f;
protected Paint mIndicatorPaint;
protected int mTabBackgroundResId;
protected int mTabOnScreenLimit;
protected int mTabMinWidth;
protected int mTabMaxWidth;
protected int mTabTextAppearance;
protected int mTabSelectedTextColor;
protected boolean mTabSelectedTextColorSet;
protected int mTabPaddingStart;
protected int mTabPaddingTop;
protected int mTabPaddingEnd;
protected int mTabPaddingBottom;
protected int mIndicatorHeight;
protected LinearLayoutManager mLinearLayoutManager;
protected RecyclerOnScrollListener mRecyclerOnScrollListener;
protected ViewPager mViewPager;
protected Adapter<?> mAdapter;
protected int mIndicatorPosition;
protected int mIndicatorGap;
protected int mIndicatorScroll;
private int mOldPosition;
private int mOldScrollOffset;
protected float mOldPositionOffset;
protected float mPositionThreshold;
protected boolean mRequestScrollToTab;
protected boolean mScrollEanbled;
public RecyclerTabLayout(Context context) {
this(context, null);
}
public RecyclerTabLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RecyclerTabLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWillNotDraw(false);
mIndicatorPaint = new Paint();
getAttributes(context, attrs, defStyle);
mLinearLayoutManager = new LinearLayoutManager(getContext()) {
@Override
public boolean canScrollHorizontally() {
return mScrollEanbled;
}
};
mLinearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
setLayoutManager(mLinearLayoutManager);
setItemAnimator(null);
mPositionThreshold = DEFAULT_POSITION_THRESHOLD;
}
private void getAttributes(Context context, AttributeSet attrs, int defStyle) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.rtl_RecyclerTabLayout,
defStyle, R.style.rtl_RecyclerTabLayout);
setIndicatorColor(a.getColor(R.styleable
.rtl_RecyclerTabLayout_rtl_tabIndicatorColor, 0));
setIndicatorHeight(a.getDimensionPixelSize(R.styleable
.rtl_RecyclerTabLayout_rtl_tabIndicatorHeight, 0));
mTabTextAppearance = a.getResourceId(R.styleable.rtl_RecyclerTabLayout_rtl_tabTextAppearance,
R.style.rtl_RecyclerTabLayout_Tab);
mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
.getDimensionPixelSize(R.styleable.rtl_RecyclerTabLayout_rtl_tabPadding, 0);
mTabPaddingStart = a.getDimensionPixelSize(
R.styleable.rtl_RecyclerTabLayout_rtl_tabPaddingStart, mTabPaddingStart);
mTabPaddingTop = a.getDimensionPixelSize(
R.styleable.rtl_RecyclerTabLayout_rtl_tabPaddingTop, mTabPaddingTop);
mTabPaddingEnd = a.getDimensionPixelSize(
R.styleable.rtl_RecyclerTabLayout_rtl_tabPaddingEnd, mTabPaddingEnd);
mTabPaddingBottom = a.getDimensionPixelSize(
R.styleable.rtl_RecyclerTabLayout_rtl_tabPaddingBottom, mTabPaddingBottom);
if (a.hasValue(R.styleable.rtl_RecyclerTabLayout_rtl_tabSelectedTextColor)) {
mTabSelectedTextColor = a
.getColor(R.styleable.rtl_RecyclerTabLayout_rtl_tabSelectedTextColor, 0);
mTabSelectedTextColorSet = true;
}
mTabOnScreenLimit = a.getInteger(
R.styleable.rtl_RecyclerTabLayout_rtl_tabOnScreenLimit, 0);
if (mTabOnScreenLimit == 0) {
mTabMinWidth = a.getDimensionPixelSize(
R.styleable.rtl_RecyclerTabLayout_rtl_tabMinWidth, 0);
mTabMaxWidth = a.getDimensionPixelSize(
R.styleable.rtl_RecyclerTabLayout_rtl_tabMaxWidth, 0);
}
mTabBackgroundResId = a
.getResourceId(R.styleable.rtl_RecyclerTabLayout_rtl_tabBackground, 0);
mScrollEanbled = a.getBoolean(R.styleable.rtl_RecyclerTabLayout_rtl_scrollEnabled, true);
a.recycle();
}
@Override
protected void onDetachedFromWindow() {
if (mRecyclerOnScrollListener != null) {
removeOnScrollListener(mRecyclerOnScrollListener);
mRecyclerOnScrollListener = null;
}
super.onDetachedFromWindow();
}
public void setIndicatorColor(int color) {
mIndicatorPaint.setColor(color);
}
public void setIndicatorHeight(int indicatorHeight) {
mIndicatorHeight = indicatorHeight;
}
public void setAutoSelectionMode(boolean autoSelect) {
if (mRecyclerOnScrollListener != null) {
removeOnScrollListener(mRecyclerOnScrollListener);
mRecyclerOnScrollListener = null;
}
if (autoSelect) {
mRecyclerOnScrollListener = new RecyclerOnScrollListener(this, mLinearLayoutManager);
addOnScrollListener(mRecyclerOnScrollListener);
}
}
public void setPositionThreshold(float positionThreshold) {
mPositionThreshold = positionThreshold;
}
public void setUpWithViewPager(ViewPager viewPager) {
DefaultAdapter adapter = new DefaultAdapter(viewPager);
adapter.setTabPadding(mTabPaddingStart, mTabPaddingTop, mTabPaddingEnd, mTabPaddingBottom);
adapter.setTabTextAppearance(mTabTextAppearance);
adapter.setTabSelectedTextColor(mTabSelectedTextColorSet, mTabSelectedTextColor);
adapter.setTabMaxWidth(mTabMaxWidth);
adapter.setTabMinWidth(mTabMinWidth);
adapter.setTabBackgroundResId(mTabBackgroundResId);
adapter.setTabOnScreenLimit(mTabOnScreenLimit);
setUpWithAdapter(adapter);
}
public void setUpWithAdapter(RecyclerTabLayout.Adapter<?> adapter) {
mAdapter = adapter;
mViewPager = adapter.getViewPager();
if (mViewPager.getAdapter() == null) {
throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
}
mViewPager.addOnPageChangeListener(new ViewPagerOnPageChangeListener(this));
setAdapter(adapter);
scrollToTab(mViewPager.getCurrentItem());
}
public void setCurrentItem(int position, boolean smoothScroll) {
if (mViewPager != null) {
mViewPager.setCurrentItem(position, smoothScroll);
scrollToTab(mViewPager.getCurrentItem());
return;
}
if (smoothScroll && position != mIndicatorPosition) {
startAnimation(position);
} else {
scrollToTab(position);
}
}
protected void startAnimation(final int position) {
float distance = 1;
View view = mLinearLayoutManager.findViewByPosition(position);
if (view != null) {
float currentX = view.getX() + view.getMeasuredWidth() / 2.f;
float centerX = getMeasuredWidth() / 2.f;
distance = Math.abs(centerX - currentX) / view.getMeasuredWidth();
}
ValueAnimator animator;
if (position < mIndicatorPosition) {
animator = ValueAnimator.ofFloat(distance, 0);
} else {
animator = ValueAnimator.ofFloat(-distance, 0);
}
animator.setDuration(DEFAULT_SCROLL_DURATION);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scrollToTab(position, (float) animation.getAnimatedValue(), true);
}
});
animator.start();
}
protected void scrollToTab(int position) {
scrollToTab(position, 0, false);
mAdapter.setCurrentIndicatorPosition(position);
mAdapter.notifyDataSetChanged();
}
protected void scrollToTab(int position, float positionOffset, boolean fitIndicator) {
int scrollOffset = 0;
View selectedView = mLinearLayoutManager.findViewByPosition(position);
View nextView = mLinearLayoutManager.findViewByPosition(position + 1);
if (selectedView != null) {
int width = getMeasuredWidth();
float sLeft = (position == 0) ? 0 : width / 2.f - selectedView.getMeasuredWidth() / 2.f; // left edge of selected tab
float sRight = sLeft + selectedView.getMeasuredWidth(); // right edge of selected tab
if (nextView != null) {
float nLeft = width / 2.f - nextView.getMeasuredWidth() / 2.f; // left edge of next tab
float distance = sRight - nLeft; // total distance that is needed to distance to next tab
float dx = distance * positionOffset;
scrollOffset = (int) (sLeft - dx);
if (position == 0) {
float indicatorGap = (nextView.getMeasuredWidth() - selectedView.getMeasuredWidth()) / 2;
mIndicatorGap = (int) (indicatorGap * positionOffset);
mIndicatorScroll = (int) ((selectedView.getMeasuredWidth() + indicatorGap) * positionOffset);
} else {
float indicatorGap = (nextView.getMeasuredWidth() - selectedView.getMeasuredWidth()) / 2;
mIndicatorGap = (int) (indicatorGap * positionOffset);
mIndicatorScroll = (int) dx;
}
} else {
scrollOffset = (int) sLeft;
mIndicatorScroll = 0;
mIndicatorGap = 0;
}
if (fitIndicator) {
mIndicatorScroll = 0;
mIndicatorGap = 0;
}
} else {
if (getMeasuredWidth() > 0 && mTabMaxWidth > 0 && mTabMinWidth == mTabMaxWidth) { //fixed size
int width = mTabMinWidth;
int offset = (int) (positionOffset * -width);
int leftOffset = (int) ((getMeasuredWidth() - width) / 2.f);
scrollOffset = offset + leftOffset;
}
mRequestScrollToTab = true;
}
updateCurrentIndicatorPosition(position, positionOffset - mOldPositionOffset, positionOffset);
mIndicatorPosition = position;
stopScroll();
if (position != mOldPosition || scrollOffset != mOldScrollOffset) {
mLinearLayoutManager.scrollToPositionWithOffset(position, scrollOffset);
}
if (mIndicatorHeight > 0) {
invalidate();
}
mOldPosition = position;
mOldScrollOffset = scrollOffset;
mOldPositionOffset = positionOffset;
}
protected void updateCurrentIndicatorPosition(int position, float dx, float positionOffset) {
if (mAdapter == null) {
return;
}
int indicatorPosition = -1;
if (dx > 0 && positionOffset >= mPositionThreshold - POSITION_THRESHOLD_ALLOWABLE) {
indicatorPosition = position + 1;
} else if (dx < 0 && positionOffset <= 1 - mPositionThreshold + POSITION_THRESHOLD_ALLOWABLE) {
indicatorPosition = position;
}
if (indicatorPosition >= 0 && indicatorPosition != mAdapter.getCurrentIndicatorPosition()) {
mAdapter.setCurrentIndicatorPosition(indicatorPosition);
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onDraw(Canvas canvas) {
View view = mLinearLayoutManager.findViewByPosition(mIndicatorPosition);
if (view == null) {
if (mRequestScrollToTab) {
mRequestScrollToTab = false;
scrollToTab(mViewPager.getCurrentItem());
}
return;
}
mRequestScrollToTab = false;
int left;
int right;
if (isLayoutRtl()) {
left = view.getLeft() - mIndicatorScroll - mIndicatorGap;
right = view.getRight() - mIndicatorScroll + mIndicatorGap;
} else {
left = view.getLeft() + mIndicatorScroll - mIndicatorGap;
right = view.getRight() + mIndicatorScroll + mIndicatorGap;
}
int top = getHeight() - mIndicatorHeight;
int bottom = getHeight();
canvas.drawRect(left, top, right, bottom, mIndicatorPaint);
}
protected boolean isLayoutRtl() {
return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
}
protected static class RecyclerOnScrollListener extends OnScrollListener {
protected RecyclerTabLayout mRecyclerTabLayout;
protected LinearLayoutManager mLinearLayoutManager;
public RecyclerOnScrollListener(RecyclerTabLayout recyclerTabLayout,
LinearLayoutManager linearLayoutManager) {
mRecyclerTabLayout = recyclerTabLayout;
mLinearLayoutManager = linearLayoutManager;
}
public int mDx;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mDx += dx;
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
switch (newState) {
case SCROLL_STATE_IDLE:
if (mDx > 0) {
selectCenterTabForRightScroll();
} else {
selectCenterTabForLeftScroll();
}
mDx = 0;
break;
case SCROLL_STATE_DRAGGING:
case SCROLL_STATE_SETTLING:
}
}
protected void selectCenterTabForRightScroll() {
int first = mLinearLayoutManager.findFirstVisibleItemPosition();
int last = mLinearLayoutManager.findLastVisibleItemPosition();
int center = mRecyclerTabLayout.getWidth() / 2;
for (int position = first; position <= last; position++) {
View view = mLinearLayoutManager.findViewByPosition(position);
if (view.getLeft() + view.getWidth() >= center) {
mRecyclerTabLayout.setCurrentItem(position, false);
break;
}
}
}
protected void selectCenterTabForLeftScroll() {
int first = mLinearLayoutManager.findFirstVisibleItemPosition();
int last = mLinearLayoutManager.findLastVisibleItemPosition();
int center = mRecyclerTabLayout.getWidth() / 2;
for (int position = last; position >= first; position--) {
View view = mLinearLayoutManager.findViewByPosition(position);
if (view.getLeft() <= center) {
mRecyclerTabLayout.setCurrentItem(position, false);
break;
}
}
}
}
protected static class ViewPagerOnPageChangeListener implements ViewPager.OnPageChangeListener {
private final RecyclerTabLayout mRecyclerTabLayout;
private int mScrollState;
public ViewPagerOnPageChangeListener(RecyclerTabLayout recyclerTabLayout) {
mRecyclerTabLayout = recyclerTabLayout;
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
mRecyclerTabLayout.scrollToTab(position, positionOffset, false);
}
@Override
public void onPageScrollStateChanged(int state) {
mScrollState = state;
}
@Override
public void onPageSelected(int position) {
if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
if (mRecyclerTabLayout.mIndicatorPosition != position) {
mRecyclerTabLayout.scrollToTab(position);
}
}
}
}
public static abstract class Adapter<T extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<T> {
protected ViewPager mViewPager;
protected int mIndicatorPosition;
public Adapter(ViewPager viewPager) {
mViewPager = viewPager;
}
public ViewPager getViewPager() {
return mViewPager;
}
public void setCurrentIndicatorPosition(int indicatorPosition) {
mIndicatorPosition = indicatorPosition;
}
public int getCurrentIndicatorPosition() {
return mIndicatorPosition;
}
}
public static class DefaultAdapter
extends RecyclerTabLayout.Adapter<DefaultAdapter.ViewHolder> {
protected static final int MAX_TAB_TEXT_LINES = 2;
protected int mTabPaddingStart;
protected int mTabPaddingTop;
protected int mTabPaddingEnd;
protected int mTabPaddingBottom;
protected int mTabTextAppearance;
protected boolean mTabSelectedTextColorSet;
protected int mTabSelectedTextColor;
private int mTabMaxWidth;
private int mTabMinWidth;
private int mTabBackgroundResId;
private int mTabOnScreenLimit;
public DefaultAdapter(ViewPager viewPager) {
super(viewPager);
}
@SuppressWarnings("deprecation")
@Override
public DefaultAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
TabTextView tabTextView = new TabTextView(parent.getContext());
if (mTabSelectedTextColorSet) {
tabTextView.setTextColor(tabTextView.createColorStateList(
tabTextView.getCurrentTextColor(), mTabSelectedTextColor));
}
ViewCompat.setPaddingRelative(tabTextView, mTabPaddingStart, mTabPaddingTop,
mTabPaddingEnd, mTabPaddingBottom);
tabTextView.setTextAppearance(parent.getContext(), mTabTextAppearance);
tabTextView.setGravity(Gravity.CENTER);
tabTextView.setMaxLines(MAX_TAB_TEXT_LINES);
tabTextView.setEllipsize(TextUtils.TruncateAt.END);
if (mTabOnScreenLimit > 0) {
int width = parent.getMeasuredWidth() / mTabOnScreenLimit;
tabTextView.setMaxWidth(width);
tabTextView.setMinWidth(width);
} else {
if (mTabMaxWidth > 0) {
tabTextView.setMaxWidth(mTabMaxWidth);
}
tabTextView.setMinWidth(mTabMinWidth);
}
tabTextView.setTextAppearance(tabTextView.getContext(), mTabTextAppearance);
if (mTabSelectedTextColorSet) {
tabTextView.setTextColor(tabTextView.createColorStateList(
tabTextView.getCurrentTextColor(), mTabSelectedTextColor));
}
if (mTabBackgroundResId != 0) {
tabTextView.setBackgroundDrawable(
AppCompatResources.getDrawable(tabTextView.getContext(), mTabBackgroundResId));
}
tabTextView.setLayoutParams(createLayoutParamsForTabs());
return new ViewHolder(tabTextView);
}
@Override
public void onBindViewHolder(DefaultAdapter.ViewHolder holder, int position) {
CharSequence title = getViewPager().getAdapter().getPageTitle(position);
holder.title.setText(title);
holder.title.setSelected(getCurrentIndicatorPosition() == position);
}
@Override
public int getItemCount() {
return getViewPager().getAdapter().getCount();
}
public void setTabPadding(int tabPaddingStart, int tabPaddingTop, int tabPaddingEnd,
int tabPaddingBottom) {
mTabPaddingStart = tabPaddingStart;
mTabPaddingTop = tabPaddingTop;
mTabPaddingEnd = tabPaddingEnd;
mTabPaddingBottom = tabPaddingBottom;
}
public void setTabTextAppearance(int tabTextAppearance) {
mTabTextAppearance = tabTextAppearance;
}
public void setTabSelectedTextColor(boolean tabSelectedTextColorSet,
int tabSelectedTextColor) {
mTabSelectedTextColorSet = tabSelectedTextColorSet;
mTabSelectedTextColor = tabSelectedTextColor;
}
public void setTabMaxWidth(int tabMaxWidth) {
mTabMaxWidth = tabMaxWidth;
}
public void setTabMinWidth(int tabMinWidth) {
mTabMinWidth = tabMinWidth;
}
public void setTabBackgroundResId(int tabBackgroundResId) {
mTabBackgroundResId = tabBackgroundResId;
}
public void setTabOnScreenLimit(int tabOnScreenLimit) {
mTabOnScreenLimit = tabOnScreenLimit;
}
protected RecyclerView.LayoutParams createLayoutParamsForTabs() {
return new RecyclerView.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
}
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView title;
public ViewHolder(View itemView) {
super(itemView);
title = (TextView) itemView;
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int pos = getAdapterPosition();
if (pos != NO_POSITION) {
getViewPager().setCurrentItem(pos, true);
}
}
});
}
}
}
public static class TabTextView extends AppCompatTextView {
public TabTextView(Context context) {
super(context);
}
public ColorStateList createColorStateList(int defaultColor, int selectedColor) {
final int[][] states = new int[2][];
final int[] colors = new int[2];
states[0] = SELECTED_STATE_SET;
colors[0] = selectedColor;
// Default enabled state
states[1] = EMPTY_STATE_SET;
colors[1] = defaultColor;
return new ColorStateList(states, colors);
}
}
}

View File

@ -5,7 +5,6 @@ import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import io.neoterm.R
import io.neoterm.component.config.NeoPreference
import io.neoterm.utils.runApt
/**
* @author kiva
@ -18,35 +17,16 @@ class GeneralSettingsActivity : BasePreferenceActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
addPreferencesFromResource(R.xml.setting_general)
val currentShell = NeoPreference.getLoginShellName()
findPreference(getString(R.string.key_general_shell)).setOnPreferenceChangeListener { _, value ->
val shellName = value.toString()
val newShell = NeoPreference.findLoginProgram(shellName)
if (newShell == null) {
requestInstallShell(shellName, currentShell)
} else {
//直接设置shell路径
postChangeShell(shellName)
}
return@setOnPreferenceChangeListener true
}
}
private fun postChangeShell(shellName: String) = NeoPreference.setLoginShellName(shellName)
private fun requestInstallShell(shellName: String, currentShell: String) {
AlertDialog.Builder(this)
.setTitle(getString(R.string.shell_not_found, shellName))
.setMessage(R.string.shell_not_found_message)
.setPositiveButton(R.string.install) { _, _ ->
runApt("install", "-y", shellName) {
it.onSuccess { postChangeShell(shellName) }
}
}
.setNegativeButton(android.R.string.no, null)
.setOnDismissListener { postChangeShell(currentShell) }
.show()
}
override fun onBuildHeaders(target: MutableList<Header>?) {
}

View File

@ -32,9 +32,6 @@ import io.neoterm.component.session.XParameter
import io.neoterm.component.session.XSession
import io.neoterm.frontend.session.terminal.*
import io.neoterm.services.NeoTermService
import io.neoterm.setup.SetupHelper
import io.neoterm.ui.other.SetupActivity
import io.neoterm.ui.pm.PackageManagerActivity
import io.neoterm.ui.settings.SettingActivity
import io.neoterm.utils.FullScreenHelper
import io.neoterm.utils.NeoPermission
@ -159,10 +156,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
startActivity(Intent(this, SettingActivity::class.java))
true
}
R.id.menu_item_package_settings -> {
startActivity(Intent(this, PackageManagerActivity::class.java))
true
}
R.id.menu_item_new_session -> {
addNewSession()
true
@ -337,11 +330,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
if (!isRecreating()) {
if (SetupHelper.needSetup()) {
val intent = Intent(this, SetupActivity::class.java)
startActivityForResult(intent, REQUEST_SETUP)
return
}
enterMain()
update_colors()
}
@ -353,7 +341,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
when (resultCode) {
AppCompatActivity.RESULT_OK -> enterMain()
AppCompatActivity.RESULT_CANCELED -> {
setSystemShellMode(true)
forceAddSystemSession()
}
}
@ -383,12 +370,10 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
// Fore system shell mode to be enabled.
addNewSession(null, true, createRevealAnimation())
addNewSession(null, createRevealAnimation())
}
private fun enterMain() {
setSystemShellMode(false)
if (!termService!!.sessions.isEmpty()) {
val lastSession = getStoredCurrentSessionOrLast()
@ -403,8 +388,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
if (intent?.action == Intent.ACTION_RUN) {
// app shortcuts
addNewSession(
null,
false, createRevealAnimation()
null, createRevealAnimation()
)
} else {
switchToSession(lastSession)
@ -413,13 +397,12 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
} else {
toggleSwitcher(showSwitcher = true, easterEgg = false)
// Fore system shell mode to be disabled.
addNewSession(null, false, createRevealAnimation())
addNewSession(null, createRevealAnimation())
}
}
override fun recreate() {
NeoPreference.store(KEY_NO_RESTORE, true)
saveCurrentStatus()
super.recreate()
}
@ -431,10 +414,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
return result
}
private fun saveCurrentStatus() {
setSystemShellMode(getSystemShellMode())
}
private fun peekRecreating(): Boolean {
return NeoPreference.loadBoolean(KEY_NO_RESTORE, false)
}
@ -476,21 +455,21 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
private fun addNewSession() = addNewSessionWithProfile(ShellProfile.create())
private fun addNewSession(sessionName: String?, systemShell: Boolean, animation: Animation) =
addNewSessionWithProfile(sessionName, systemShell, animation, ShellProfile.create())
private fun addNewSession(sessionName: String?, animation: Animation) =
addNewSessionWithProfile(sessionName, animation, ShellProfile.create())
private fun addNewSessionWithProfile(profile: ShellProfile) {
if (!tabSwitcher.isSwitcherShown) {
toggleSwitcher(showSwitcher = true, easterEgg = false)
}
addNewSessionWithProfile(
null, getSystemShellMode(),
null,
createRevealAnimation(), profile
)
}
private fun addNewSessionWithProfile(
sessionName: String?, systemShell: Boolean,
sessionName: String?,
animation: Animation, profile: ShellProfile
) {
val sessionCallback = TermSessionCallback()
@ -498,7 +477,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
val parameter = ShellParameter()
.callback(sessionCallback)
.systemShell(systemShell)
.profile(profile)
val session = termService!!.createTermSession(parameter)
@ -698,14 +676,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
}
private fun setSystemShellMode(systemShell: Boolean) {
NeoPreference.store(NeoPreference.KEY_SYSTEM_SHELL, systemShell)
}
private fun getSystemShellMode(): Boolean {
return NeoPreference.loadBoolean(NeoPreference.KEY_SYSTEM_SHELL, true)
}
private inline fun <reified T> forEachTab(callback: (T) -> Unit) {
(0 until tabSwitcher.count)
.map { tabSwitcher.getTab(it) }

View File

@ -223,7 +223,6 @@ class NeoTermRemoteInterface : AppCompatActivity(), ServiceConnection {
val parameter = ShellParameter()
.initialCommand(initialCommand)
.callback(TermSessionCallback())
.systemShell(detectSystemShell())
.session(sessionId)
openTerm(parameter, foreground)
}
@ -234,7 +233,6 @@ class NeoTermRemoteInterface : AppCompatActivity(), ServiceConnection {
.arguments(arguments)
.currentWorkingDirectory(cwd)
.callback(TermSessionCallback())
.systemShell(detectSystemShell())
openTerm(parameter)
}

View File

@ -51,24 +51,6 @@ fun Context.extractAssetsDir(assetDir: String, extractDir: String) = kotlin.runC
}
}
fun Context.runApt(
subCommand: String, vararg extraArgs: String,
autoClose: Boolean = true, block: (Result<TerminalDialog>) -> Unit
) = TerminalDialog(this)
.execute(NeoTermPath.APT_BIN_PATH, arrayOf(NeoTermPath.APT_BIN_PATH, subCommand, *extraArgs))
.imeEnabled(true)
.onFinish { dialog, session ->
val exit = session?.exitStatus ?: 1
if (exit == 0) {
if (autoClose) dialog.dismiss()
block(Result.success(dialog))
} else {
dialog.setTitle(getString(R.string.error))
block(Result.failure(RuntimeException()))
}
}
.show("apt $subCommand")
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and

View File

@ -176,89 +176,6 @@
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="@color/list_download_item_color_dark"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="72dp"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:scaleType="centerCrop"
app:srcCompat="@mipmap/ic_danger"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="@string/dangerous_zone"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"/>
</LinearLayout>
<LinearLayout
android:id="@+id/about_reset_app_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
app:srcCompat="@drawable/ic_info"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="32dp"
android:layout_marginStart="32dp"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingTop="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_reset_label"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_reset_label_desc"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/terminal_background"
android:orientation="vertical">
<LinearLayout
android:id="@+id/pm_tab_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/pm_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
app:popupTheme="@style/ThemeOverlay.AppCompat.Dark"/>
<io.neoterm.ui.pm.view.RecyclerTabLayout
android:id="@+id/pm_tab_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/recycler_tab_height"
android:background="@color/colorPrimary"/>
</LinearLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/pm_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/pm_tab_header"/>
</RelativeLayout>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/terminal_background"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/pm_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
app:popupTheme="@style/ThemeOverlay.AppCompat.Dark"/>
<include layout="@layout/layout_pm_package_list"/>
</LinearLayout>

View File

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="32dp"
android:orientation="vertical">
<TextView
android:id="@+id/setup_title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="26dp"
android:layout_marginTop="26dp"
android:gravity="center"
android:text="@string/setup_info"
android:textColor="@color/colorAccent"
android:textSize="32sp"/>
<TextView
android:id="@+id/select_method_tip_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/setup_title_text"
android:text="@string/setup_setup_method"/>
<Button
android:id="@+id/setup_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="@string/setup_next"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/setup_next"
android:layout_below="@id/select_method_tip_text"
android:orientation="vertical">
<RadioGroup
android:id="@+id/setup_method_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkedButton="@id/setup_method_online">
<RadioButton
android:id="@+id/setup_method_online"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_online"/>
<RadioButton
android:id="@+id/setup_method_local"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_local"/>
<RadioButton
android:id="@+id/setup_method_backup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_backup"/>
</RadioGroup>
<TextView
android:id="@+id/setup_url_tip_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/setup_source_parameter"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/setup_source_parameter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:focusable="false"
android:clickable="false"
android:hint="@string/setup_hint_online"
android:text="@string/default_source_url"/>
<Button
android:id="@+id/setup_source_parameter_select"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:text="@string/setup_dots"/>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/setup_log_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
</LinearLayout>
</RelativeLayout>

View File

@ -35,11 +35,6 @@
</menu>
</item>
<item
android:id="@+id/menu_item_package_settings"
android:title="@string/package_settings"
app:showAsAction="never"/>
<item
android:id="@+id/menu_item_settings"
android:title="@string/settings"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#607D8B</color>
<color name="colorPrimaryDark">#455A64</color>
<color name="colorPrimary">#19B4FF</color>
<color name="colorPrimaryDark">#22A2DF</color>
<color name="colorAccent">#42a5f5</color>
<color name="terminal_background">#ff14181c</color>
<color name="textColor">#fefefe</color>

View File

@ -181,19 +181,9 @@
<string name="sorry_for_development">This feature is still under development so it is only available on DEBUG
builds.\n
</string>
<string name="dangerous_zone">Dangerous Zone</string>
<string name="reset_app_warning">You will have to re-setup later, confirm?</string>
<string name="default_source_url" translatable="false">https://raw.githubusercontent.com/NeoTerm/NeoTerm-repo/main
</string>
<string-array name="pref_general_shell_entries" translatable="false">
<item>sh</item>
<item>zsh</item>
<item>bash</item>
<item>fish</item>
</string-array>
<string-array name="pref_package_source_values" translatable="false">
<item>@string/default_source_url</item>
</string-array>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="bash"
android:entries="@array/pref_general_shell_entries"
android:entryValues="@array/pref_general_shell_entries"
<EditTextPreference
android:key="@string/key_general_shell"
android:summary="@string/pref_general_shell_desc"
android:title="@string/pref_general_shell"/>

View File

@ -28,15 +28,6 @@
android:targetPackage="io.neoterm"/>
</Preference>
<Preference
android:icon="@drawable/ic_apps_white_36dp"
android:summary="@string/package_settings_desc"
android:title="@string/package_settings">
<intent
android:targetClass="io.neoterm.ui.pm.PackageManagerActivity"
android:targetPackage="io.neoterm"/>
</Preference>
<Preference
android:icon="@drawable/ic_info_white_36dp"
android:title="@string/about">