This commit is contained in:
parent
236072395c
commit
0e9d9dc0e8
@ -134,21 +134,11 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/error"
|
android:label="@string/error"
|
||||||
android:theme="@style/AppTheme.NoActionBar.Dark"/>
|
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
|
<activity
|
||||||
android:name=".ui.other.BonusActivity"
|
android:name=".ui.other.BonusActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/AppTheme.NoActionBar.Dark"/>
|
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
|
<activity
|
||||||
android:name=".ui.customize.CustomizeActivity"
|
android:name=".ui.customize.CustomizeActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -7,12 +7,11 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
||||||
static const char *rewrite_executable(const char *filename, char *buffer, int buffer_len) {
|
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/");
|
char *bin_match = strstr(filename, "/bin/");
|
||||||
if (bin_match == filename || bin_match == (filename + 4)) {
|
if (bin_match == filename || bin_match == (filename + 4)) {
|
||||||
// We have either found "/bin/" at the start of the string or at
|
// We have either found "/bin/" at the start of the string or at
|
||||||
// "/xxx/bin/". Take the path after that.
|
// "/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;
|
filename = buffer;
|
||||||
}
|
}
|
||||||
return filename;
|
return filename;
|
||||||
|
@ -108,7 +108,7 @@ static int create_subprocess(JNIEnv *env,
|
|||||||
// Show terminal output about failing exec() call:
|
// Show terminal output about failing exec() call:
|
||||||
char *error_message;
|
char *error_message;
|
||||||
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1)
|
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1)
|
||||||
error_message = const_cast<char *>("exec()");;
|
error_message = const_cast<char *>("exec()");
|
||||||
perror(error_message);
|
perror(error_message);
|
||||||
_exit(1);
|
_exit(1);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import io.neoterm.component.completion.CompletionComponent
|
|||||||
import io.neoterm.component.config.ConfigureComponent
|
import io.neoterm.component.config.ConfigureComponent
|
||||||
import io.neoterm.component.extrakey.ExtraKeyComponent
|
import io.neoterm.component.extrakey.ExtraKeyComponent
|
||||||
import io.neoterm.component.font.FontComponent
|
import io.neoterm.component.font.FontComponent
|
||||||
import io.neoterm.component.pm.PackageComponent
|
|
||||||
import io.neoterm.component.profile.ProfileComponent
|
import io.neoterm.component.profile.ProfileComponent
|
||||||
import io.neoterm.component.session.SessionComponent
|
import io.neoterm.component.session.SessionComponent
|
||||||
import io.neoterm.component.session.ShellProfile
|
import io.neoterm.component.session.ShellProfile
|
||||||
@ -80,7 +79,6 @@ object NeoInitializer {
|
|||||||
ComponentManager.registerComponent(UserScriptComponent::class.java)
|
ComponentManager.registerComponent(UserScriptComponent::class.java)
|
||||||
ComponentManager.registerComponent(ExtraKeyComponent::class.java)
|
ComponentManager.registerComponent(ExtraKeyComponent::class.java)
|
||||||
ComponentManager.registerComponent(CompletionComponent::class.java)
|
ComponentManager.registerComponent(CompletionComponent::class.java)
|
||||||
ComponentManager.registerComponent(PackageComponent::class.java)
|
|
||||||
ComponentManager.registerComponent(SessionComponent::class.java)
|
ComponentManager.registerComponent(SessionComponent::class.java)
|
||||||
ComponentManager.registerComponent(ProfileComponent::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)
|
val dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f, context.resources.displayMetrics)
|
||||||
MIN_FONT_SIZE = (4f * dipInPixels).toInt()
|
MIN_FONT_SIZE = (4f * dipInPixels).toInt()
|
||||||
MAX_FONT_SIZE = 256
|
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) {
|
fun store(key: Int, value: Any) {
|
||||||
@ -156,7 +144,6 @@ object NeoPreference {
|
|||||||
val loginProgramPath = findLoginProgram(loginProgramName) ?: return false
|
val loginProgramPath = findLoginProgram(loginProgramName) ?: return false
|
||||||
|
|
||||||
store(R.string.key_general_shell, loginProgramName)
|
store(R.string.key_general_shell, loginProgramName)
|
||||||
symlinkLoginShell(loginProgramPath)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,39 +154,17 @@ object NeoPreference {
|
|||||||
fun getLoginShellPath(): String {
|
fun getLoginShellPath(): String {
|
||||||
val loginProgramName = getLoginShellName()
|
val loginProgramName = getLoginShellName()
|
||||||
|
|
||||||
// Some programs like ssh needs it
|
|
||||||
val shell = File(NeoTermPath.NEOTERM_LOGIN_SHELL_PATH)
|
|
||||||
val loginProgramPath = findLoginProgram(loginProgramName) ?: {
|
val loginProgramPath = findLoginProgram(loginProgramName) ?: {
|
||||||
setLoginShellName(DefaultValues.loginShell)
|
setLoginShellName(DefaultValues.loginShell)
|
||||||
"${NeoTermPath.USR_PATH}/bin/${DefaultValues.loginShell}"
|
loadString(R.string.key_general_shell,DefaultValues.loginShell)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if (!shell.exists()) {
|
|
||||||
symlinkLoginShell(loginProgramPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return loginProgramPath
|
return loginProgramPath
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateFontSize(fontSize: Int): Int {
|
fun validateFontSize(fontSize: Int): Int {
|
||||||
return Math.max(MIN_FONT_SIZE, Math.min(fontSize, MAX_FONT_SIZE))
|
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? {
|
fun findLoginProgram(loginProgramName: String): String? {
|
||||||
val file = File("${NeoTermPath.USR_PATH}/bin", loginProgramName)
|
val file = File("${NeoTermPath.USR_PATH}/bin", loginProgramName)
|
||||||
return if (file.canExecute()) file.absolutePath else null
|
return if (file.canExecute()) file.absolutePath else null
|
||||||
|
@ -18,7 +18,7 @@ object DefaultValues {
|
|||||||
const val enableSpecialVolumeKeys = false
|
const val enableSpecialVolumeKeys = false
|
||||||
const val enableWordBasedIme = false
|
const val enableWordBasedIme = false
|
||||||
|
|
||||||
const val loginShell = "bash"
|
const val loginShell = "sh"
|
||||||
const val initialCommand = ""
|
const val initialCommand = ""
|
||||||
const val defaultFont = "SourceCodePro"
|
const val defaultFont = "SourceCodePro"
|
||||||
}
|
}
|
||||||
@ -28,7 +28,6 @@ object NeoTermPath {
|
|||||||
const val ROOT_PATH = "/data/data/io.neoterm/files"
|
const val ROOT_PATH = "/data/data/io.neoterm/files"
|
||||||
const val USR_PATH = "$ROOT_PATH/usr"
|
const val USR_PATH = "$ROOT_PATH/usr"
|
||||||
const val HOME_PATH = "$ROOT_PATH/home"
|
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 LIB_PATH = "$USR_PATH/lib"
|
||||||
|
|
||||||
const val CUSTOM_PATH = "$HOME_PATH/.neoterm"
|
const val CUSTOM_PATH = "$HOME_PATH/.neoterm"
|
||||||
@ -40,14 +39,4 @@ object NeoTermPath {
|
|||||||
const val USER_SCRIPT_PATH = "$CUSTOM_PATH/script"
|
const val USER_SCRIPT_PATH = "$CUSTOM_PATH/script"
|
||||||
const val PROFILE_PATH = "$CUSTOM_PATH/profile"
|
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)
|
.executablePath(parameter.executablePath)
|
||||||
.currentWorkingDirectory(parameter.cwd)
|
.currentWorkingDirectory(parameter.cwd)
|
||||||
.callback(parameter.sessionCallback)
|
.callback(parameter.sessionCallback)
|
||||||
.systemShell(parameter.systemShell)
|
|
||||||
.envArray(parameter.env)
|
.envArray(parameter.env)
|
||||||
.argArray(parameter.arguments)
|
.argArray(parameter.arguments)
|
||||||
.initialCommand(parameter.initialCommand)
|
.initialCommand(parameter.initialCommand)
|
||||||
|
@ -27,7 +27,6 @@ class ShellParameter {
|
|||||||
var initialCommand: String? = null
|
var initialCommand: String? = null
|
||||||
var env: Array<Pair<String, String>>? = null
|
var env: Array<Pair<String, String>>? = null
|
||||||
var sessionCallback: TerminalSession.SessionChangedCallback? = null
|
var sessionCallback: TerminalSession.SessionChangedCallback? = null
|
||||||
var systemShell: Boolean = false
|
|
||||||
var shellProfile: ShellProfile? = null
|
var shellProfile: ShellProfile? = null
|
||||||
|
|
||||||
fun executablePath(executablePath: String?): ShellParameter {
|
fun executablePath(executablePath: String?): ShellParameter {
|
||||||
@ -60,11 +59,6 @@ class ShellParameter {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun systemShell(systemShell: Boolean): ShellParameter {
|
|
||||||
this.systemShell = systemShell
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun profile(shellProfile: ShellProfile): ShellParameter {
|
fun profile(shellProfile: ShellProfile): ShellParameter {
|
||||||
this.shellProfile = shellProfile
|
this.shellProfile = shellProfile
|
||||||
return this
|
return this
|
||||||
@ -291,21 +285,14 @@ open class ShellTermSession private constructor(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun systemShell(systemShell: Boolean): Builder {
|
|
||||||
this.systemShell = systemShell
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create(context: Context): ShellTermSession {
|
fun create(context: Context): ShellTermSession {
|
||||||
val cwd = this.cwd ?: NeoTermPath.HOME_PATH
|
val cwd = this.cwd ?: NeoTermPath.HOME_PATH
|
||||||
|
|
||||||
val shell = this.executablePath ?: if (systemShell)
|
val shell = shellProfile.loginShell
|
||||||
"/system/bin/sh"
|
|
||||||
else
|
|
||||||
shellProfile.loginShell
|
|
||||||
|
|
||||||
val args = this.args ?: mutableListOf(shell)
|
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()
|
val callback = changeCallback ?: TermSessionCallback()
|
||||||
return ShellTermSession(
|
return ShellTermSession(
|
||||||
shell, cwd, args.toTypedArray(), env, callback,
|
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
|
val selectedCwd = cwd ?: NeoTermPath.HOME_PATH
|
||||||
File(NeoTermPath.HOME_PATH).mkdirs()
|
File(NeoTermPath.HOME_PATH).mkdirs()
|
||||||
|
|
||||||
@ -336,62 +323,11 @@ open class ShellTermSession private constructor(
|
|||||||
val externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE")
|
val externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE")
|
||||||
val colorterm = "COLORTERM=truecolor"
|
val colorterm = "COLORTERM=truecolor"
|
||||||
|
|
||||||
// PY Trade: Some programs support NeoTerm in a special way.
|
val pathEnv = "PATH=" + System.getenv("PATH")
|
||||||
val neotermIdEnv = "__NEOTERM=1"
|
return arrayOf(
|
||||||
val originPathEnv = "__NEOTERM_ORIGIN_PATH=" + buildOriginPathEnv()
|
termEnv, homeEnv, androidRootEnv, androidDataEnv,
|
||||||
val originLdEnv = "__NEOTERM_ORIGIN_LD_LIBRARY_PATH=" + buildOriginLdLibEnv()
|
externalStorageEnv, pathEnv, prefixEnv, colorterm
|
||||||
|
).filter { it.isNotEmpty() }.toTypedArray()
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,6 @@ class TerminalDialog(val context: Context) {
|
|||||||
.executablePath(executablePath)
|
.executablePath(executablePath)
|
||||||
.arguments(arguments)
|
.arguments(arguments)
|
||||||
.callback(terminalSessionCallback)
|
.callback(terminalSessionCallback)
|
||||||
.systemShell(false)
|
|
||||||
terminalSession = Terminals.createSession(context, parameter)
|
terminalSession = Terminals.createSession(context, parameter)
|
||||||
if (terminalSession is ShellTermSession) {
|
if (terminalSession is ShellTermSession) {
|
||||||
(terminalSession as ShellTermSession).exitPrompt = context.getString(R.string.process_exit_prompt_press_back)
|
(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")
|
.executablePath("${NeoTermPath.USR_PATH}/bin/echo")
|
||||||
.arguments(arrayOf("echo", "-e", *script))
|
.arguments(arrayOf("echo", "-e", *script))
|
||||||
.callback(sessionCallback)
|
.callback(sessionCallback)
|
||||||
.systemShell(false)
|
|
||||||
|
|
||||||
session = Terminals.createSession(this, parameter)
|
session = Terminals.createSession(this, parameter)
|
||||||
terminalView.attachSession(session)
|
terminalView.attachSession(session)
|
||||||
|
@ -131,20 +131,6 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
findViewById<View>(R.id.about_source_code_view).setOnClickListener {
|
findViewById<View>(R.id.about_source_code_view).setOnClickListener {
|
||||||
openUrl("https://github.com/NeoTerm/NeoTerm")
|
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) {
|
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 androidx.appcompat.app.AlertDialog
|
||||||
import io.neoterm.R
|
import io.neoterm.R
|
||||||
import io.neoterm.component.config.NeoPreference
|
import io.neoterm.component.config.NeoPreference
|
||||||
import io.neoterm.utils.runApt
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kiva
|
* @author kiva
|
||||||
@ -18,35 +17,16 @@ class GeneralSettingsActivity : BasePreferenceActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
addPreferencesFromResource(R.xml.setting_general)
|
addPreferencesFromResource(R.xml.setting_general)
|
||||||
|
|
||||||
val currentShell = NeoPreference.getLoginShellName()
|
|
||||||
findPreference(getString(R.string.key_general_shell)).setOnPreferenceChangeListener { _, value ->
|
findPreference(getString(R.string.key_general_shell)).setOnPreferenceChangeListener { _, value ->
|
||||||
val shellName = value.toString()
|
val shellName = value.toString()
|
||||||
val newShell = NeoPreference.findLoginProgram(shellName)
|
//直接设置shell路径
|
||||||
if (newShell == null) {
|
postChangeShell(shellName)
|
||||||
requestInstallShell(shellName, currentShell)
|
|
||||||
} else {
|
|
||||||
postChangeShell(shellName)
|
|
||||||
}
|
|
||||||
return@setOnPreferenceChangeListener true
|
return@setOnPreferenceChangeListener true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postChangeShell(shellName: String) = NeoPreference.setLoginShellName(shellName)
|
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>?) {
|
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.component.session.XSession
|
||||||
import io.neoterm.frontend.session.terminal.*
|
import io.neoterm.frontend.session.terminal.*
|
||||||
import io.neoterm.services.NeoTermService
|
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.ui.settings.SettingActivity
|
||||||
import io.neoterm.utils.FullScreenHelper
|
import io.neoterm.utils.FullScreenHelper
|
||||||
import io.neoterm.utils.NeoPermission
|
import io.neoterm.utils.NeoPermission
|
||||||
@ -159,10 +156,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
startActivity(Intent(this, SettingActivity::class.java))
|
startActivity(Intent(this, SettingActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.menu_item_package_settings -> {
|
|
||||||
startActivity(Intent(this, PackageManagerActivity::class.java))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_item_new_session -> {
|
R.id.menu_item_new_session -> {
|
||||||
addNewSession()
|
addNewSession()
|
||||||
true
|
true
|
||||||
@ -337,11 +330,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isRecreating()) {
|
if (!isRecreating()) {
|
||||||
if (SetupHelper.needSetup()) {
|
|
||||||
val intent = Intent(this, SetupActivity::class.java)
|
|
||||||
startActivityForResult(intent, REQUEST_SETUP)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enterMain()
|
enterMain()
|
||||||
update_colors()
|
update_colors()
|
||||||
}
|
}
|
||||||
@ -353,7 +341,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
AppCompatActivity.RESULT_OK -> enterMain()
|
AppCompatActivity.RESULT_OK -> enterMain()
|
||||||
AppCompatActivity.RESULT_CANCELED -> {
|
AppCompatActivity.RESULT_CANCELED -> {
|
||||||
setSystemShellMode(true)
|
|
||||||
forceAddSystemSession()
|
forceAddSystemSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -383,12 +370,10 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fore system shell mode to be enabled.
|
// Fore system shell mode to be enabled.
|
||||||
addNewSession(null, true, createRevealAnimation())
|
addNewSession(null, createRevealAnimation())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enterMain() {
|
private fun enterMain() {
|
||||||
setSystemShellMode(false)
|
|
||||||
|
|
||||||
if (!termService!!.sessions.isEmpty()) {
|
if (!termService!!.sessions.isEmpty()) {
|
||||||
val lastSession = getStoredCurrentSessionOrLast()
|
val lastSession = getStoredCurrentSessionOrLast()
|
||||||
|
|
||||||
@ -403,8 +388,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
if (intent?.action == Intent.ACTION_RUN) {
|
if (intent?.action == Intent.ACTION_RUN) {
|
||||||
// app shortcuts
|
// app shortcuts
|
||||||
addNewSession(
|
addNewSession(
|
||||||
null,
|
null, createRevealAnimation()
|
||||||
false, createRevealAnimation()
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
switchToSession(lastSession)
|
switchToSession(lastSession)
|
||||||
@ -413,13 +397,12 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
} else {
|
} else {
|
||||||
toggleSwitcher(showSwitcher = true, easterEgg = false)
|
toggleSwitcher(showSwitcher = true, easterEgg = false)
|
||||||
// Fore system shell mode to be disabled.
|
// Fore system shell mode to be disabled.
|
||||||
addNewSession(null, false, createRevealAnimation())
|
addNewSession(null, createRevealAnimation())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recreate() {
|
override fun recreate() {
|
||||||
NeoPreference.store(KEY_NO_RESTORE, true)
|
NeoPreference.store(KEY_NO_RESTORE, true)
|
||||||
saveCurrentStatus()
|
|
||||||
super.recreate()
|
super.recreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,10 +414,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveCurrentStatus() {
|
|
||||||
setSystemShellMode(getSystemShellMode())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun peekRecreating(): Boolean {
|
private fun peekRecreating(): Boolean {
|
||||||
return NeoPreference.loadBoolean(KEY_NO_RESTORE, false)
|
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() = addNewSessionWithProfile(ShellProfile.create())
|
||||||
|
|
||||||
private fun addNewSession(sessionName: String?, systemShell: Boolean, animation: Animation) =
|
private fun addNewSession(sessionName: String?, animation: Animation) =
|
||||||
addNewSessionWithProfile(sessionName, systemShell, animation, ShellProfile.create())
|
addNewSessionWithProfile(sessionName, animation, ShellProfile.create())
|
||||||
|
|
||||||
private fun addNewSessionWithProfile(profile: ShellProfile) {
|
private fun addNewSessionWithProfile(profile: ShellProfile) {
|
||||||
if (!tabSwitcher.isSwitcherShown) {
|
if (!tabSwitcher.isSwitcherShown) {
|
||||||
toggleSwitcher(showSwitcher = true, easterEgg = false)
|
toggleSwitcher(showSwitcher = true, easterEgg = false)
|
||||||
}
|
}
|
||||||
addNewSessionWithProfile(
|
addNewSessionWithProfile(
|
||||||
null, getSystemShellMode(),
|
null,
|
||||||
createRevealAnimation(), profile
|
createRevealAnimation(), profile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addNewSessionWithProfile(
|
private fun addNewSessionWithProfile(
|
||||||
sessionName: String?, systemShell: Boolean,
|
sessionName: String?,
|
||||||
animation: Animation, profile: ShellProfile
|
animation: Animation, profile: ShellProfile
|
||||||
) {
|
) {
|
||||||
val sessionCallback = TermSessionCallback()
|
val sessionCallback = TermSessionCallback()
|
||||||
@ -498,7 +477,6 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
|
|||||||
|
|
||||||
val parameter = ShellParameter()
|
val parameter = ShellParameter()
|
||||||
.callback(sessionCallback)
|
.callback(sessionCallback)
|
||||||
.systemShell(systemShell)
|
|
||||||
.profile(profile)
|
.profile(profile)
|
||||||
val session = termService!!.createTermSession(parameter)
|
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) {
|
private inline fun <reified T> forEachTab(callback: (T) -> Unit) {
|
||||||
(0 until tabSwitcher.count)
|
(0 until tabSwitcher.count)
|
||||||
.map { tabSwitcher.getTab(it) }
|
.map { tabSwitcher.getTab(it) }
|
||||||
|
@ -223,7 +223,6 @@ class NeoTermRemoteInterface : AppCompatActivity(), ServiceConnection {
|
|||||||
val parameter = ShellParameter()
|
val parameter = ShellParameter()
|
||||||
.initialCommand(initialCommand)
|
.initialCommand(initialCommand)
|
||||||
.callback(TermSessionCallback())
|
.callback(TermSessionCallback())
|
||||||
.systemShell(detectSystemShell())
|
|
||||||
.session(sessionId)
|
.session(sessionId)
|
||||||
openTerm(parameter, foreground)
|
openTerm(parameter, foreground)
|
||||||
}
|
}
|
||||||
@ -234,7 +233,6 @@ class NeoTermRemoteInterface : AppCompatActivity(), ServiceConnection {
|
|||||||
.arguments(arguments)
|
.arguments(arguments)
|
||||||
.currentWorkingDirectory(cwd)
|
.currentWorkingDirectory(cwd)
|
||||||
.callback(TermSessionCallback())
|
.callback(TermSessionCallback())
|
||||||
.systemShell(detectSystemShell())
|
|
||||||
openTerm(parameter)
|
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
|
* 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
|
* Framework Documents, as well as the _data field for the MediaStore and
|
||||||
|
@ -176,89 +176,6 @@
|
|||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</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>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</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>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/menu_item_package_settings"
|
|
||||||
android:title="@string/package_settings"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_item_settings"
|
android:id="@+id/menu_item_settings"
|
||||||
android:title="@string/settings"
|
android:title="@string/settings"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#607D8B</color>
|
<color name="colorPrimary">#19B4FF</color>
|
||||||
<color name="colorPrimaryDark">#455A64</color>
|
<color name="colorPrimaryDark">#22A2DF</color>
|
||||||
<color name="colorAccent">#42a5f5</color>
|
<color name="colorAccent">#42a5f5</color>
|
||||||
<color name="terminal_background">#ff14181c</color>
|
<color name="terminal_background">#ff14181c</color>
|
||||||
<color name="textColor">#fefefe</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
|
<string name="sorry_for_development">This feature is still under development so it is only available on DEBUG
|
||||||
builds.\n
|
builds.\n
|
||||||
</string>
|
</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 name="default_source_url" translatable="false">https://raw.githubusercontent.com/NeoTerm/NeoTerm-repo/main
|
||||||
</string>
|
</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">
|
<string-array name="pref_package_source_values" translatable="false">
|
||||||
<item>@string/default_source_url</item>
|
<item>@string/default_source_url</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<ListPreference
|
<EditTextPreference
|
||||||
android:defaultValue="bash"
|
|
||||||
android:entries="@array/pref_general_shell_entries"
|
|
||||||
android:entryValues="@array/pref_general_shell_entries"
|
|
||||||
android:key="@string/key_general_shell"
|
android:key="@string/key_general_shell"
|
||||||
android:summary="@string/pref_general_shell_desc"
|
android:summary="@string/pref_general_shell_desc"
|
||||||
android:title="@string/pref_general_shell"/>
|
android:title="@string/pref_general_shell"/>
|
||||||
@ -55,4 +52,4 @@
|
|||||||
android:summary="@string/pref_general_auto_completion_desc"
|
android:summary="@string/pref_general_auto_completion_desc"
|
||||||
android:title="@string/pref_general_auto_completion"/>
|
android:title="@string/pref_general_auto_completion"/>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
@ -28,15 +28,6 @@
|
|||||||
android:targetPackage="io.neoterm"/>
|
android:targetPackage="io.neoterm"/>
|
||||||
</Preference>
|
</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
|
<Preference
|
||||||
android:icon="@drawable/ic_info_white_36dp"
|
android:icon="@drawable/ic_info_white_36dp"
|
||||||
android:title="@string/about">
|
android:title="@string/about">
|
||||||
|
Loading…
Reference in New Issue
Block a user