This commit is contained in:
parent
236072395c
commit
0e9d9dc0e8
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
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"
|
||||
val pathEnv = "PATH=" + System.getenv("PATH")
|
||||
return arrayOf(
|
||||
termEnv, homeEnv, androidRootEnv, androidDataEnv,
|
||||
externalStorageEnv, pathEnv, prefixEnv, colorterm
|
||||
).filter { it.isNotEmpty() }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
postChangeShell(shellName)
|
||||
}
|
||||
//直接设置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>?) {
|
||||
}
|
||||
|
||||
|
@ -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) }
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -35,14 +35,9 @@
|
||||
</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"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
</menu>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
@ -55,4 +52,4 @@
|
||||
android:summary="@string/pref_general_auto_completion_desc"
|
||||
android:title="@string/pref_general_auto_completion"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user