PackageManager: Support multi-source

This commit is contained in:
imkiva 2018-02-15 23:44:38 +08:00
parent 9f9f856bf9
commit 23937764f9
36 changed files with 2332 additions and 163 deletions

View File

@ -46,6 +46,10 @@ android {
abortOnError false abortOnError false
checkReleaseBuilds false checkReleaseBuilds false
} }
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
} }
dependencies { dependencies {

View File

@ -94,3 +94,6 @@
java.lang.Object readResolve(); java.lang.Object readResolve();
} }
-keep class * extends io.neoterm.framework.database.annotation.* { *; }
-keep interface * extends io.neoterm.framework.database.annotation.* { *; }

View File

@ -7,16 +7,14 @@ package io.neoterm.component.pm
enum class Architecture { enum class Architecture {
ALL, ARM, AARCH64, X86, X86_64; ALL, ARM, AARCH64, X86, X86_64;
companion object { companion object {
fun parse(arch: String): Architecture { fun parse(arch: String): Architecture {
when (arch) { return when (arch) {
"arm" -> return ARM "arm" -> ARM
"aarch64" -> return AARCH64 "aarch64" -> AARCH64
"x86" -> return X86 "x86" -> X86
"x86_64" -> return X86_64 "x86_64" -> X86_64
else -> return ALL else -> ALL
} }
} }
} }

View File

@ -22,7 +22,7 @@ public class PackageComponent implements NeoComponent {
} }
public HashMap<String, NeoPackageInfo> getPackages() { public HashMap<String, NeoPackageInfo> getPackages() {
return queryEnabled ? neoPackages : new HashMap<String, NeoPackageInfo>(); return queryEnabled ? neoPackages : new HashMap<>();
} }
public int getPackageCount() { public int getPackageCount() {

View File

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

View File

@ -0,0 +1,73 @@
package io.neoterm.component.pm
import io.neoterm.frontend.component.ComponentManager
import io.neoterm.frontend.config.NeoTermPath
import io.neoterm.frontend.logging.NLog
import io.neoterm.utils.FileUtils
import java.io.File
import java.net.URL
/**
* @author kiva
*/
object SourceHelper {
fun syncSource() {
val sourceManager = ComponentManager.getComponent<PackageComponent>().sourceManager
syncSource(sourceManager)
}
fun syncSource(sourceManager: SourceManager) {
val sourceFile = File(NeoTermPath.SOURCE_FILE)
val content = buildString {
this.append("# Generated by NeoTerm-Preference\n")
sourceManager.getEnabledSources()
.joinTo(this, "\n") { "deb ${it.url} ${it.repo}\n" }
}
FileUtils.writeFile(sourceFile, 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 ""
}
}
}

View File

@ -2,26 +2,53 @@ package io.neoterm.component.pm
import io.neoterm.App import io.neoterm.App
import io.neoterm.R import io.neoterm.R
import io.neoterm.frontend.config.NeoPreference import io.neoterm.framework.NeoTermDatabase
import io.neoterm.frontend.config.NeoTermPath
/** /**
* @author kiva * @author kiva
*/ */
class SourceManager internal constructor() { class SourceManager internal constructor() {
val sources = mutableSetOf<String>() private val database = NeoTermDatabase.instance("sources")
init { init {
NeoPreference.loadStrings(NeoPreference.KEY_SOURCES).mapTo(sources, { it }) if (database.findAll<Source>(Source::class.java).isEmpty()) {
if (sources.isEmpty()) { App.get().resources.getStringArray(R.array.pref_package_source_values)
sources.addAll(App.get().resources.getStringArray(R.array.pref_package_source_values)) .forEach {
database.saveBean(Source(it, "stable main", true))
}
} }
} }
fun addSource(sourceUrl: String) { fun addSource(sourceUrl: String, repo: String, enabled: Boolean) {
sources.add(sourceUrl) 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>(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() { fun applyChanges() {
NeoPreference.storeStrings(NeoPreference.KEY_SOURCES, sources) database.vacuum()
} }
} }

View File

@ -1,57 +0,0 @@
package io.neoterm.component.pm
import io.neoterm.R
import io.neoterm.frontend.config.NeoPreference
import io.neoterm.frontend.config.NeoTermPath
import io.neoterm.frontend.logging.NLog
import java.io.File
import java.net.URL
/**
* @author kiva
*/
object SourceUtils {
fun detectSourceFiles(): ArrayList<File> {
val sourceFiles = ArrayList<File>()
try {
val sourceUrl = NeoPreference.loadString(R.string.key_package_source, NeoTermPath.DEFAULT_SOURCE)
val packageFilePrefix = detectSourceFilePrefix(sourceUrl)
if (packageFilePrefix.isNotEmpty()) {
File(NeoTermPath.PACKAGE_LIST_DIR)
.listFiles()
.filterTo(sourceFiles) { it.name.startsWith(packageFilePrefix) }
}
} catch (e: Exception) {
sourceFiles.clear()
NLog.e("PM", "Failed to detect source files: ${e.localizedMessage}")
}
return sourceFiles
}
fun detectSourceFilePrefix(sourceUrl: String): String {
try {
val url = URL(sourceUrl)
val builder = StringBuilder()
builder.append(url.host)
// https://github.com/NeoTerm/NeoTerm/issues/1
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_stable_main_binary-")
return builder.toString()
} catch (e: Exception) {
NLog.e("PM", "Failed to detect source file prefix: ${e.localizedMessage}")
return ""
}
}
}

View File

@ -0,0 +1,675 @@
package io.neoterm.framework;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.neoterm.App;
import io.neoterm.framework.database.DatabaseDataType;
import io.neoterm.framework.database.OnDatabaseUpgradedListener;
import io.neoterm.framework.database.SQLStatementHelper;
import io.neoterm.framework.database.SQLTypeParser;
import io.neoterm.framework.database.NeoTermSQLiteConfig;
import io.neoterm.framework.database.TableHelper;
import io.neoterm.framework.database.ValueHelper;
import io.neoterm.framework.database.bean.TableInfo;
import io.neoterm.framework.reflection.Reflect;
import io.neoterm.frontend.logging.NLog;
/**
* @author Lody, Kiva
* <p>
* 基于<b>DTO(DataToObject)</b>映射的数据库操纵模型.
* 通过少量可选的注解,即可构造数据模型.
* 增删查改异常轻松.
* @version 1.4
*/
public class NeoTermDatabase {
/**
* 缓存创建的数据库,以便防止数据库冲突.
*/
private static final Map<String, NeoTermDatabase> DAO_MAP = new HashMap<>();
/**
* 数据库配置
*/
private NeoTermSQLiteConfig neoTermSQLiteConfig;
/**
* 内部操纵的数据库执行类
*/
private SQLiteDatabase db;
/**
* 默认构造器
*
* @param config
*/
private NeoTermDatabase(NeoTermSQLiteConfig config) {
this.neoTermSQLiteConfig = config;
String saveDir = config.getSaveDir();
if (saveDir != null
&& saveDir.trim().length() > 0) {
this.db = createDataBaseFileOnSDCard(saveDir,
config.getDatabaseName());
} else {
this.db = new SQLiteDataBaseHelper(App.Companion.get()
.getApplicationContext()
.getApplicationContext(), config)
.getWritableDatabase();
}
}
/**
* 根据配置取得用于操纵数据库的WeLikeDao实例
*
* @param config
* @return
*/
public static NeoTermDatabase instance(NeoTermSQLiteConfig config) {
if (config.getDatabaseName() == null) {
throw new IllegalArgumentException("DBName is null in SqLiteConfig.");
}
NeoTermDatabase dao = DAO_MAP.get(config.getDatabaseName());
if (dao == null) {
dao = new NeoTermDatabase(config);
synchronized (DAO_MAP) {
DAO_MAP.put(config.getDatabaseName(), dao);
}
} else {//更换配置
dao.applyConfig(config);
}
return dao;
}
/**
* 根据默认配置取得操纵数据库的WeLikeDao实例
*
* @return
*/
public static NeoTermDatabase instance() {
return instance(NeoTermSQLiteConfig.DEFAULT_CONFIG);
}
/**
* 取得操纵数据库的WeLikeDao实例
*
* @param dbName
* @return
*/
public static NeoTermDatabase instance(String dbName) {
NeoTermSQLiteConfig config = new NeoTermSQLiteConfig();
config.setDatabaseName(dbName);
return instance(config);
}
/**
* 取得操纵数据库的WeLikeDao实例
*
* @param dbVersion
* @return
*/
public static NeoTermDatabase instance(int dbVersion) {
NeoTermSQLiteConfig config = new NeoTermSQLiteConfig();
config.setDatabaseVersion(dbVersion);
return instance(config);
}
/**
* 取得操纵数据库的WeLikeDao实例
*
* @param listener
* @return
*/
public static NeoTermDatabase instance(OnDatabaseUpgradedListener listener) {
NeoTermSQLiteConfig config = new NeoTermSQLiteConfig();
config.setOnDatabaseUpgradedListener(listener);
return instance(config);
}
/**
* 取得操纵数据库的WeLikeDao实例
*
* @param dbName
* @param dbVersion
* @return
*/
public static NeoTermDatabase instance(String dbName, int dbVersion) {
NeoTermSQLiteConfig config = new NeoTermSQLiteConfig();
config.setDatabaseName(dbName);
config.setDatabaseVersion(dbVersion);
return instance(config);
}
/**
* 取得操纵数据库的WeLikeDao实例
*
* @param dbName
* @param dbVersion
* @param listener
* @return
*/
public static NeoTermDatabase instance(String dbName, int dbVersion, OnDatabaseUpgradedListener listener) {
NeoTermSQLiteConfig config = new NeoTermSQLiteConfig();
config.setDatabaseName(dbName);
config.setDatabaseVersion(dbVersion);
config.setOnDatabaseUpgradedListener(listener);
return instance(config);
}
/**
* 配置为新的参数(不改变数据库名).
*
* @param config
*/
private void applyConfig(NeoTermSQLiteConfig config) {
this.neoTermSQLiteConfig.debugMode = config.debugMode;
this.neoTermSQLiteConfig.setOnDatabaseUpgradedListener(config.getOnDatabaseUpgradedListener());
}
public void release() {
DAO_MAP.clear();
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.d("缓存的DAO已经全部清除,将不占用内存.");
}
}
/**
* 在SD卡的指定目录上创建数据库文件
*
* @param sdcardPath sd卡路径
* @param dbFileName 数据库文件名
* @return
*/
private SQLiteDatabase createDataBaseFileOnSDCard(String sdcardPath,
String dbFileName) {
File dbFile = new File(sdcardPath, dbFileName);
if (!dbFile.exists()) {
try {
if (dbFile.createNewFile()) {
return SQLiteDatabase.openOrCreateDatabase(dbFile, null);
}
} catch (IOException e) {
throw new RuntimeException("无法在 " + dbFile.getAbsolutePath() + "创建DB文件.");
}
} else {
//数据库文件已经存在,无需再次创建.
return SQLiteDatabase.openOrCreateDatabase(dbFile, null);
}
return null;
}
/**
* 如果表不存在,需要创建它.
*
* @param clazz
*/
private void createTableIfNeed(Class<?> clazz) {
TableInfo tableInfo = TableHelper.from(clazz);
if (tableInfo.isCreate) {
return;
}
if (!isTableExist(tableInfo)) {
String sql = SQLStatementHelper.createTable(tableInfo);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(sql);
}
db.execSQL(sql);
Method afterTableCreateMethod = tableInfo.afterTableCreateMethod;
if (afterTableCreateMethod != null) {
//如果afterTableMethod存在,就调用它
try {
afterTableCreateMethod.invoke(null, this);
} catch (Throwable ignore) {
ignore.printStackTrace();
}
}
}
}
/**
* 判断表是否存在?
*
* @param table 需要盘的的表
* @return
*/
private boolean isTableExist(TableInfo table) {
String sql = "SELECT COUNT(*) AS c FROM sqlite_master WHERE type ='table' AND name ='"
+ table.tableName + "' ";
try (Cursor cursor = db.rawQuery(sql, null)) {
if (cursor != null && cursor.moveToNext()) {
int count = cursor.getInt(0);
if (count > 0) {
return true;
}
}
} catch (Throwable ignore) {
ignore.printStackTrace();
}
return false;
}
/**
* 删除全部的表
*/
public void dropAllTable() {
try (Cursor cursor = db.rawQuery(
"SELECT name FROM sqlite_master WHERE type ='table'", null)) {
if (cursor != null) {
cursor.moveToFirst();
while (cursor.moveToNext()) {
try {
dropTable(cursor.getString(0));
} catch (SQLException ignore) {
ignore.printStackTrace();
}
}
}
}
}
/**
* 取得数据库中的表的数量
*
* @return 表的数量
*/
public int tableCount() {
try (Cursor cursor = db.rawQuery(
"SELECT name FROM sqlite_master WHERE type ='table'", null)) {
return cursor == null ? 0 : cursor.getCount();
}
}
/**
* 取得数据库中的所有表名组成的List.
*
* @return
*/
public List<String> getTableList() {
try (Cursor cursor = db.rawQuery(
"SELECT name FROM sqlite_master WHERE type ='table'", null)) {
List<String> tableList = new ArrayList<>();
if (cursor != null) {
cursor.moveToFirst();
while (cursor.moveToNext()) {
tableList.add(cursor.getString(0));
}
}
return tableList;
}
}
/**
* 删除一张表
*
* @param beanClass 表所对应的类
*/
public void dropTable(Class<?> beanClass) {
TableInfo tableInfo = TableHelper.from(beanClass);
dropTable(tableInfo.tableName);
tableInfo.isCreate = false;
}
/**
* 删除一张表
*
* @param tableName 表名
*/
public void dropTable(String tableName) {
String statement = "DROP TABLE IF EXISTS " + tableName;
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
db.execSQL(statement);
TableInfo tableInfo = TableHelper.findTableInfoByName(tableName);
if (tableInfo != null) {
tableInfo.isCreate = false;
}
}
/**
* 存储一个Bean.
*
* @param bean
* @return
*/
public <T> NeoTermDatabase saveBean(T bean) {
createTableIfNeed(bean.getClass());
String statement = SQLStatementHelper.insertIntoTable(bean);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
db.execSQL(statement);
return this;
}
/**
* 存储多个Bean.
*
* @param beans
* @return
*/
public NeoTermDatabase saveBeans(Object[] beans) {
for (Object o : beans) {
saveBean(o);
}
return this;
}
/**
* 存储多个Bean.
*
* @param beans
* @return
*/
public <T> NeoTermDatabase saveBeans(List<T> beans) {
for (Object o : beans) {
saveBean(o);
}
return this;
}
/**
* 寻找Bean对应的全部数据
*
* @param clazz
* @param <T>
* @return
*/
public <T> List<T> findAll(Class<?> clazz) {
createTableIfNeed(clazz);
TableInfo tableInfo = TableHelper.from(clazz);
String statement = SQLStatementHelper.selectTable(tableInfo.tableName);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
List<T> list = new ArrayList<T>();
try (Cursor cursor = db.rawQuery(statement, null)) {
if (cursor == null) {
// DO NOT RETURN NULL
// null checks are ugly!
return Collections.emptyList();
}
while (cursor.moveToNext()) {
T object = Reflect.on(clazz).create().get();
if (tableInfo.containID) {
DatabaseDataType dataType = SQLTypeParser.getDataType(tableInfo.primaryField);
String idFieldName = tableInfo.primaryField.getName();
ValueHelper.setKeyValue(cursor, object, tableInfo.primaryField, dataType, cursor.getColumnIndex(idFieldName));
}
for (Field field : tableInfo.fieldToDataTypeMap.keySet()) {
DatabaseDataType dataType = tableInfo.fieldToDataTypeMap.get(field);
ValueHelper.setKeyValue(cursor, object, field, dataType, cursor.getColumnIndex(field.getName()));
}
list.add(object);
}
return list;
}
}
/**
* 根据where语句寻找Bean
*
* @param clazz
* @param <T>
* @return
*/
public <T> List<T> findBeanByWhere(Class<?> clazz, String where) {
createTableIfNeed(clazz);
TableInfo tableInfo = TableHelper.from(clazz);
String statement = SQLStatementHelper.findByWhere(tableInfo, where);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
List<T> list = new ArrayList<>();
try (Cursor cursor = db.rawQuery(statement, null)) {
if (cursor == null) {
// DO NOT RETURN NULL
// null checks are ugly!
return Collections.emptyList();
}
while (cursor.moveToNext()) {
T object = Reflect.on(clazz).create().get();
if (tableInfo.containID) {
DatabaseDataType dataType = SQLTypeParser.getDataType(tableInfo.primaryField);
String idFieldName = tableInfo.primaryField.getName();
ValueHelper.setKeyValue(cursor, object, tableInfo.primaryField, dataType, cursor.getColumnIndex(idFieldName));
}
for (Field field : tableInfo.fieldToDataTypeMap.keySet()) {
DatabaseDataType dataType = tableInfo.fieldToDataTypeMap.get(field);
ValueHelper.setKeyValue(cursor, object, field, dataType, cursor.getColumnIndex(field.getName()));
}
list.add(object);
}
return list;
}
}
public <T> T findOneBeanByWhere(Class<?> clazz, String where) {
List<T> list = findBeanByWhere(clazz, where);
if (!list.isEmpty()) {
return list.get(0);
}
return null;
}
/**
* 根据where语句删除Bean
*
* @param clazz
* @return
*/
public NeoTermDatabase deleteBeanByWhere(Class<?> clazz, String where) {
createTableIfNeed(clazz);
TableInfo tableInfo = TableHelper.from(clazz);
String statement = SQLStatementHelper.deleteByWhere(tableInfo, where);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
try {
db.execSQL(statement);
} catch (SQLException ignore) {
ignore.printStackTrace();
}
return this;
}
/**
* 删除指定ID的bean
*
* @param tableClass
* @param id
* @return 删除的Bean
*/
public NeoTermDatabase deleteBeanByID(Class<?> tableClass, Object id) {
createTableIfNeed(tableClass);
TableInfo tableInfo = TableHelper.from(tableClass);
DatabaseDataType dataType = SQLTypeParser.getDataType(id.getClass());
if (dataType != null && tableInfo.primaryField != null) {
//判断ID类型是否与数据类型匹配
boolean match = SQLTypeParser.matchType(tableInfo.primaryField, dataType);
if (!match) {//不匹配,抛出异常
throw new IllegalArgumentException("类型 " + id.getClass().getName() + " 不是主键的类型,主键的类型应该为 " + tableInfo.primaryField.getType().getName());
}
}
String idValue = ValueHelper.valueToString(dataType, id);
String statement = SQLStatementHelper.deleteByWhere(tableInfo, tableInfo.primaryField == null ? "_id" : tableInfo.primaryField.getName() + " = " + idValue);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
try {
db.execSQL(statement);
} catch (SQLException ignore) {
ignore.printStackTrace();
//删除失败
}
return this;
}
/**
* 根据给定的where更新数据
*
* @param tableClass
* @param where
* @param bean
* @return
*/
public NeoTermDatabase updateByWhere(Class<?> tableClass, String where, Object bean) {
createTableIfNeed(tableClass);
TableInfo tableInfo = TableHelper.from(tableClass);
String statement = SQLStatementHelper.updateByWhere(tableInfo, bean, where);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.d(statement);
}
db.execSQL(statement);
return this;
}
/**
* 根据给定的id更新数据
*
* @param tableClass
* @param id
* @param bean
* @return
*/
public NeoTermDatabase updateByID(Class<?> tableClass, Object id, Object bean) {
createTableIfNeed(tableClass);
TableInfo tableInfo = TableHelper.from(tableClass);
StringBuilder subStatement = new StringBuilder();
if (tableInfo.containID) {
subStatement.append(tableInfo.primaryField.getName()).append(" = ").append(ValueHelper.valueToString(SQLTypeParser.getDataType(tableInfo.primaryField), id));
} else {
subStatement.append("_id = ").append((int) id);
}
updateByWhere(tableClass, subStatement.toString(), bean);
return this;
}
/**
* 根据ID查找Bean
*
* @param tableClass
* @param id
* @param <T>
* @return
*/
public <T> T findBeanByID(Class<?> tableClass, Object id) {
createTableIfNeed(tableClass);
TableInfo tableInfo = TableHelper.from(tableClass);
DatabaseDataType dataType = SQLTypeParser.getDataType(id.getClass());
if (dataType == null) {
return null;
}
// 判断ID类型是否与数据类型匹配
boolean match = SQLTypeParser.matchType(tableInfo.primaryField, dataType) || tableInfo.primaryField == null;
if (!match) {// 不匹配,抛出异常
throw new IllegalArgumentException("Type " + id.getClass().getName() + " is not the primary key, expecting " + tableInfo.primaryField.getType().getName());
}
String idValue = ValueHelper.valueToString(dataType, id);
String statement = SQLStatementHelper.findByWhere(tableInfo, tableInfo.primaryField == null ? "_id" : tableInfo.primaryField.getName() + " = " + idValue);
if (neoTermSQLiteConfig.debugMode) {
NLog.INSTANCE.w(statement);
}
try (Cursor cursor = db.rawQuery(statement, null)) {
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
T bean = Reflect.on(tableClass).create().get();
for (Field field : tableInfo.fieldToDataTypeMap.keySet()) {
DatabaseDataType fieldType = tableInfo.fieldToDataTypeMap.get(field);
ValueHelper.setKeyValue(cursor, bean, field, fieldType, cursor.getColumnIndex(field.getName()));
}
try {
Reflect.on(bean).set(tableInfo.containID ? tableInfo.primaryField.getName() : "_id", id);
} catch (Throwable ignore) {
// 我们允许Bean没有id字段,因此此异常可以忽略
}
return bean;
}
return null;
}
}
/**
* 通过 VACUUM 命令压缩数据库
*/
public void vacuum() {
db.execSQL("VACUUM");
}
/**
* 调用本方法会释放当前数据库占用的内存,
* 调用后请确保你不会在接下来的代码中继续用到本实例.
*/
public void destroy() {
DAO_MAP.remove(this);
this.neoTermSQLiteConfig = null;
this.db = null;
}
/**
* 取得内部操纵的SqliteDatabase.
*
* @return
*/
public SQLiteDatabase getDatabase() {
return db;
}
/**
* 内部数据库监听器,负责派发接口.
*/
private class SQLiteDataBaseHelper extends SQLiteOpenHelper {
private final OnDatabaseUpgradedListener onDatabaseUpgradedListener;
private final boolean defaultDropAllTables;
public SQLiteDataBaseHelper(Context context, NeoTermSQLiteConfig config) {
super(context, config.getDatabaseName(), null, config.getDatabaseVersion());
this.onDatabaseUpgradedListener = config.getOnDatabaseUpgradedListener();
this.defaultDropAllTables = config.isDefaultDropAllTables();
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (onDatabaseUpgradedListener != null) {
onDatabaseUpgradedListener.onDatabaseUpgraded(db, oldVersion, newVersion);
} else if (defaultDropAllTables) { // 干掉所有的表
dropAllTable();
}
}
}
}

View File

@ -0,0 +1,38 @@
package io.neoterm.framework.database;
/**
* @author kiva
*/
public enum DatabaseDataType {
/**
* int类型
*/
INTEGER,
/**
* String类型
*/
TEXT,
/**
* float类型
*/
FLOAT,
/**
* long类型
*/
BIGINT,
/**
* double类型
*/
DOUBLE;
boolean nullable = true;
/**
* 数据类型是否允许为null
*/
public DatabaseDataType nullable(boolean nullable) {
this.nullable = nullable;
return this;
}
}

View File

@ -0,0 +1,123 @@
package io.neoterm.framework.database;
import java.io.Serializable;
/**
* @author kiva
*/
public class NeoTermSQLiteConfig implements Serializable {
private static final long serialVersionUID = -4069725570156436316L;
//==============================================================
// 常量
//==============================================================
public static String DEFAULT_DB_NAME = "we_like.db";
public static NeoTermSQLiteConfig DEFAULT_CONFIG = new NeoTermSQLiteConfig();
//==============================================================
// 字段
//==============================================================
/**
* 是否为DEBUG模式
*/
public boolean debugMode = false;
/**
* 数据库名
*/
private String dbName = DEFAULT_DB_NAME;
/**
* 数据库升级监听器
*/
private OnDatabaseUpgradedListener onDatabaseUpgradedListener;
private boolean defaultDropAllTables = false;
private String saveDir;
private int dbVersion = 1;
/**
* 取得数据库的名称
*
* @return
*/
public String getDatabaseName() {
return dbName;
}
/**
* 设置数据库的名称
*
* @param dbName
*/
public void setDatabaseName(String dbName) {
this.dbName = dbName;
}
/**
* 取得数据库升级监听器
*
* @return
*/
public OnDatabaseUpgradedListener getOnDatabaseUpgradedListener() {
return onDatabaseUpgradedListener;
}
/**
* 设置数据库升级监听器
*
* @param onDatabaseUpgradedListener
*/
public void setOnDatabaseUpgradedListener(OnDatabaseUpgradedListener onDatabaseUpgradedListener) {
this.onDatabaseUpgradedListener = onDatabaseUpgradedListener;
}
/**
* 取得数据库保存目录
*
* @return
*/
public String getSaveDir() {
return saveDir;
}
/**
* 设置数据库的保存目录
*
* @param saveDir
*/
public void setSaveDir(String saveDir) {
this.saveDir = saveDir;
}
/**
* 获取DB的版本号
*
* @return
*/
public int getDatabaseVersion() {
return dbVersion;
}
/**
* 设置DB的版本号
*
* @param dbVersion
*/
public void setDatabaseVersion(int dbVersion) {
this.dbVersion = dbVersion;
}
/**
* App 更新时是否默认删除所有存在的表
* @return
*/
public boolean isDefaultDropAllTables() {
return defaultDropAllTables;
}
/**
* 设置 App 更新时是否默认删除所有存在的表
* @param defaultDropAllTables
*/
public void setDefaultDropAllTables(boolean defaultDropAllTables) {
this.defaultDropAllTables = defaultDropAllTables;
}
}

View File

@ -0,0 +1,15 @@
package io.neoterm.framework.database;
import android.database.sqlite.SQLiteDatabase;
/**
* @author kiva
*/
public interface OnDatabaseUpgradedListener {
/**
* @param db 数据库
* @param oldVersion 旧版本
* @param newVersion 新版本
*/
void onDatabaseUpgraded(SQLiteDatabase db, int oldVersion, int newVersion);
}

View File

@ -0,0 +1,198 @@
package io.neoterm.framework.database;
import java.lang.reflect.Field;
import io.neoterm.framework.database.annotation.ID;
import io.neoterm.framework.database.bean.TableInfo;
/**
* @author kiva
*/
public class SQLStatementHelper {
/**
* 构造<b>创建表</b>的语句
*
* @param tableInfo 表信息
* @return 创建表的SQL语句
*/
public static String createTable(TableInfo tableInfo) {
StringBuilder statement = new StringBuilder();
statement.append("CREATE TABLE ").append("'")
.append(tableInfo.tableName).append("'")
.append(" (");
if (tableInfo.containID) {
DatabaseDataType dataType = SQLTypeParser.getDataType(tableInfo.primaryField);
if (dataType == null) {
throw new IllegalArgumentException("Type of " + tableInfo.primaryField.getType().getName() + " is not support in WelikeDB.");
}
statement.append("'").append(tableInfo.primaryField.getName()).append("'");
switch (dataType) {
case INTEGER:
statement.append(" INTEGER PRIMARY KEY ");
ID id = tableInfo.primaryField.getAnnotation(ID.class);
if (id != null && id.autoIncrement()) {
statement.append("AUTOINCREMENT");
}
break;
default:
statement
.append(" ")
.append(dataType.name())
.append(" PRIMARY KEY");
}
statement.append(",");
} else {
statement.append("'_id' INTEGER PRIMARY KEY AUTOINCREMENT,");
}
for (Field field : tableInfo.fieldToDataTypeMap.keySet()) {
DatabaseDataType dataType = tableInfo.fieldToDataTypeMap.get(field);
statement.append("'").append(field.getName()).append("'")
.append(" ")
.append(dataType.name());
if (!dataType.nullable) {
statement.append(" NOT NULL");
}
statement.append(",");
}
//删掉最后一个逗号
statement.deleteCharAt(statement.length() - 1);
statement.append(")");
return statement.toString();
}
/**
* 构建 插入一个Bean 的语句.
*
* @param o
* @return
*/
public static String insertIntoTable(Object o) {
TableInfo tableInfo = TableHelper.from(o.getClass());
StringBuilder statement = new StringBuilder();
statement.append("INSERT INTO ").append(tableInfo.tableName).append(" ");
statement.append("VALUES(");
if (tableInfo.containID) {
DatabaseDataType primaryDataType = SQLTypeParser.getDataType(tableInfo.primaryField);
switch (primaryDataType) {
case INTEGER:
statement.append("NULL,");
break;
default:
try {
statement
.append(ValueHelper.valueToString(primaryDataType, tableInfo.primaryField, o))
.append(",");
} catch (IllegalAccessException ignored) {
}
break;
}
} else {
statement.append("NULL,");
}
for (Field field : tableInfo.fieldToDataTypeMap.keySet()) {
DatabaseDataType dataType = tableInfo.fieldToDataTypeMap.get(field);
try {
statement.append(ValueHelper.valueToString(dataType, field, o)).append(",");
} catch (IllegalAccessException e) {
//不会发生...
}
}
statement.deleteCharAt(statement.length() - 1);
statement.append(")");
return statement.toString();
}
/**
* 根据where条件创建选择语句
*
* @param tableInfo
* @param where
* @return
*/
public static String findByWhere(TableInfo tableInfo, String where) {
StringBuilder statement = new StringBuilder("SELECT * FROM ");
statement
.append(tableInfo.tableName)
.append(" ")
.append("WHERE ")
.append(where);
return statement.toString();
}
/**
* 根据where条件创建删除语句
*
* @param tableInfo
* @param where
* @return
*/
public static String deleteByWhere(TableInfo tableInfo, String where) {
StringBuilder statement = new StringBuilder("DELETE FROM ");
statement
.append(tableInfo.tableName)
.append(" ")
.append("WHERE ")
.append(where);
return statement.toString();
}
/**
* 根据where条件创建更新语句
*
* @param tableInfo
* @param bean
* @param where
* @return
*/
public static String updateByWhere(TableInfo tableInfo, Object bean, String where) {
StringBuilder builder = new StringBuilder("UPDATE ");
builder.append(tableInfo.tableName).append(" SET ");
for (Field f : tableInfo.fieldToDataTypeMap.keySet()) {
try {
builder.append(f.getName())
.append(" = ")
.append(ValueHelper.valueToString(
SQLTypeParser.getDataType(f.getType()),
f.get(bean))).append(",");
} catch (Throwable ignored) {
}
}
builder.deleteCharAt(builder.length() - 1);//删除最后一个逗号
builder.append(" WHERE ");
builder.append(where);
return builder.toString();
}
/**
* 创建选中table的语句
*
* @param tableName
* @return
*/
public static String selectTable(String tableName) {
return "SELECT * FROM " + tableName;
}
}

View File

@ -0,0 +1,81 @@
package io.neoterm.framework.database;
import java.lang.reflect.Field;
import io.neoterm.framework.database.annotation.Ignore;
import io.neoterm.framework.database.annotation.NotNull;
/**
* @author kiva
*/
public class SQLTypeParser {
/**
* 根据字段类型匹配它在数据库中的对应类型.
*
* @param field
* @return
*/
public static DatabaseDataType getDataType(Field field) {
Class<?> clazz = field.getType();
if (clazz == (String.class)) {
return DatabaseDataType.TEXT.nullable((field.getAnnotation(NotNull.class) == null));
} else if (clazz == (int.class) || clazz == (Integer.class)) {
return DatabaseDataType.INTEGER.nullable((field.getAnnotation(NotNull.class) == null));
} else if (clazz == (float.class) || clazz == (Float.class)) {
return DatabaseDataType.FLOAT.nullable((field.getAnnotation(NotNull.class) == null));
} else if (clazz == (long.class) || clazz == (Long.class)) {
return DatabaseDataType.BIGINT.nullable((field.getAnnotation(NotNull.class) == null));
} else if (clazz == (double.class) || clazz == (Double.class)) {
return DatabaseDataType.DOUBLE.nullable((field.getAnnotation(NotNull.class) == null));
} else if (clazz == (boolean.class) || clazz == (Boolean.class)) {
return DatabaseDataType.INTEGER.nullable((field.getAnnotation(NotNull.class) == null));
}
return null;
}
/**
* 根据字段类型匹配它在数据库中的对应类型.
*
* @param clazz
* @return
*/
public static DatabaseDataType getDataType(Class<?> clazz) {
if (clazz == (String.class)) {
return DatabaseDataType.TEXT;
} else if (clazz == (int.class) || clazz == (Integer.class)) {
return DatabaseDataType.INTEGER;
} else if (clazz == (float.class) || clazz == (Float.class)) {
return DatabaseDataType.FLOAT;
} else if (clazz == (long.class) || clazz == (Long.class)) {
return DatabaseDataType.BIGINT;
} else if (clazz == (double.class) || clazz == (Double.class)) {
return DatabaseDataType.DOUBLE;
} else if (clazz == (boolean.class) || clazz == (Boolean.class)) {
return DatabaseDataType.INTEGER;
}
return null;
}
/**
* 字段类型与数据类型是否匹配?
*
* @param field
* @param dataType
* @return
*/
public static boolean matchType(Field field, DatabaseDataType dataType) {
DatabaseDataType fieldDataType = getDataType(field.getType());
return dataType != null && fieldDataType == (dataType);
}
/**
* 字段是否可以被数据库忽略?
*
* @param field
* @return
*/
public static boolean isIgnore(Field field) {
return field.getAnnotation(Ignore.class) != null;
}
}

View File

@ -0,0 +1,132 @@
package io.neoterm.framework.database;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import io.neoterm.framework.NeoTermDatabase;
import io.neoterm.framework.database.annotation.ID;
import io.neoterm.framework.database.annotation.Table;
import io.neoterm.framework.database.bean.TableInfo;
/**
* @author kiva
*/
public class TableHelper {
private static final Map<Class<?>, TableInfo> classToTableInfoMap = new HashMap<>();
/**
* 根据传入的Bean的Class将其映射为一个TableInfo.
*
* @param clazz
* @return
*/
public static TableInfo from(Class<?> clazz) {
TableInfo tableInfo = classToTableInfoMap.get(clazz);
if (tableInfo != null) {
return tableInfo;
}
tableInfo = new TableInfo();
//Table注解解析
Table table = clazz.getAnnotation(Table.class);
String afterTableCreateMethod = table != null ? table.afterTableCreate() : null;
if (afterTableCreateMethod != null && afterTableCreateMethod.trim().length() > 0) {
try {
Method method = clazz.getDeclaredMethod(afterTableCreateMethod, NeoTermDatabase.class);
if (method != null && Modifier.isStatic(method.getModifiers())) {
method.setAccessible(true);
tableInfo.afterTableCreateMethod = method;
}
} catch (Throwable ignored) {
}
}
if (table != null && table.name().trim().length() != 0) {
tableInfo.tableName = table.name();
} else {
tableInfo.tableName = clazz.getName().replace(".", "_");
}
Map<Field, DatabaseDataType> fieldEnumMap = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
//如果这个字段加了ignore注解,我们就跳过
if (SQLTypeParser.isIgnore(field)) {
continue;
}
DatabaseDataType dataType = SQLTypeParser.getDataType(field);
if (dataType != null) {
fieldEnumMap.put(field, dataType);
} else {
throw new IllegalArgumentException("The type of " + field.getName() + " is not supported in database.");
}
}
tableInfo.fieldToDataTypeMap = fieldEnumMap;
buildPrimaryIDForTableInfo(tableInfo);
tableInfo.createTableStatement = SQLStatementHelper.createTable(tableInfo);
synchronized (classToTableInfoMap) {
classToTableInfoMap.put(clazz, tableInfo);
}
return tableInfo;
}
/**
* 为一个Bean匹配一个ID字段,如果ID字段不存在,使用默认的_id替代.
*
* @param info
* @return
*/
private static TableInfo buildPrimaryIDForTableInfo(TableInfo info) {
Field idField = null;
ID id;
for (Field field : info.fieldToDataTypeMap.keySet()) {
id = field.getAnnotation(ID.class);
if (id != null) {
idField = field;
break;
}
}//end
if (idField != null) {
//从字段表中移除ID
info.fieldToDataTypeMap.remove(idField);
info.containID = true;
info.primaryField = idField;
} else {
info.containID = false;
info.primaryField = null;
}
return info;
}
/**
* 根据表名匹配TableInfo
*
* @param tableName
* @return
*/
public static TableInfo findTableInfoByName(String tableName) {
for (TableInfo tableInfo : classToTableInfoMap.values()) {
if (tableInfo.tableName.equals(tableName)) {
return tableInfo;
}
}
return null;
}
/**
* 清除留在内存中的TableInfo缓存
*/
public static void clearCache() {
classToTableInfoMap.clear();
}
}

View File

@ -0,0 +1,118 @@
package io.neoterm.framework.database;
import android.database.Cursor;
import java.lang.reflect.Field;
/**
* @author kiva
*/
public class ValueHelper {
/**
* 根据数据类型将数据库中的值写入到相应的字段.
*
* @param cursor 游标
* @param object 赋值对象
* @param field 赋值字段
* @param dataType 数据类型
*/
public static void setKeyValue(Cursor cursor, Object object, Field field, DatabaseDataType dataType, int index) {
switch (dataType) {
case INTEGER:
try {
field.set(object, cursor.getInt(index));
} catch (Throwable e) {
try {
//支持Boolean类型
//因为Boolean默认当Integer处理
field.set(object, cursor.getInt(index) != 0);
} catch (IllegalAccessException ignored) {
}
}
break;
case TEXT:
try {
field.set(object, cursor.getString(index));
} catch (IllegalAccessException e) {
}
break;
case FLOAT:
try {
field.set(object, cursor.getFloat(index));
} catch (IllegalAccessException e) {
}
break;
case BIGINT:
try {
field.set(object, cursor.getLong(index));
} catch (IllegalAccessException e) {
}
break;
case DOUBLE:
try {
field.set(object, cursor.getDouble(index));
} catch (IllegalAccessException e) {
}
break;
}
}
/**
* 根据数据类型从字段中提取值并转换为String
*
* @param dataType
* @param field
* @param o
* @return
* @throws IllegalAccessException 无法转换时抛出的异常
*/
public static String valueToString(DatabaseDataType dataType, Field field, Object o) throws IllegalAccessException {
switch (dataType) {
case INTEGER:
Object f = field.get(o);
if (f instanceof Boolean) {
return String.valueOf(((boolean) field.get(o)) ? 1 : 0);
} else {
return String.valueOf((int) field.get(o));
}
case TEXT:
return "\"" + field.get(o) + "" + "\"";
case DOUBLE:
return String.valueOf((double) field.get(o));
case FLOAT:
return String.valueOf((float) field.get(o));
case BIGINT:
return String.valueOf((long) field.get(o));
}
return null;
}
/**
* 根据数据类型将对象转换为String
*
* @param dataType
* @param o
* @return
*/
public static String valueToString(DatabaseDataType dataType, Object o) {
switch (dataType) {
case INTEGER:
if (o instanceof Boolean) {
return ((boolean) o) ? "1" : "0";
} else {
return String.valueOf((int) o);
}
case TEXT:
return "\"" + o + "\"";
case DOUBLE:
return String.valueOf((double) o);
case FLOAT:
return String.valueOf((float) o);
case BIGINT:
return String.valueOf((long) o);
}
return null;
}
}

View File

@ -0,0 +1,20 @@
package io.neoterm.framework.database.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author kiva
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ID {
/**
* 只对Integer类型的ID字段有效
*
* @return 是否为自增长
*/
boolean autoIncrement() default false;
}

View File

@ -0,0 +1,14 @@
package io.neoterm.framework.database.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author kiva
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Ignore {
}

View File

@ -0,0 +1,14 @@
package io.neoterm.framework.database.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author kiva
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
}

View File

@ -0,0 +1,23 @@
package io.neoterm.framework.database.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author kiva
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
/**
* @return 表名
*/
String name() default "";
/**
* @return 在表创建后需要回调的方法
*/
String afterTableCreate() default "";
}

View File

@ -0,0 +1,46 @@
package io.neoterm.framework.database.bean;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import io.neoterm.framework.database.DatabaseDataType;
/**
* @author kiva
*/
public class TableInfo {
/**
* 是否包含ID
*/
public boolean containID;
/**
* 主键字段
*/
public Field primaryField;
/**
* 表名
*/
public String tableName;
/**
* 字段表
*/
public Map<Field, DatabaseDataType> fieldToDataTypeMap;
/**
* 创建table的语句
*/
public String createTableStatement;
/**
* 是否已经创建
*/
public boolean isCreate = false;
public Method afterTableCreateMethod;
}

View File

@ -0,0 +1,8 @@
package io.neoterm.framework.reflection;
/**
* class representing null pointer.
* @author kiva
*/
public class NullPointer {
}

View File

@ -0,0 +1,560 @@
package io.neoterm.framework.reflection;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Make reflections easier and elegant.
*
* @author kiva
*/
public class Reflect {
private final Object mObject;
private final boolean isClass;
private Reflect(Class<?> type) {
this.mObject = type;
this.isClass = true;
}
private Reflect(Object object) {
this.mObject = object;
this.isClass = false;
}
/**
* Create reflector from class name.
*
* @param name Full class name
* @return Reflector
* @throws ReflectionException If any error occurs
* @see #on(Class)
*/
public static Reflect on(String name) throws ReflectionException {
return on(forName(name));
}
/**
* Create reflector from class name using given class loader.
*
* @param name Full class name
* @param classLoader Given class loader
* @return Reflector
* @throws ReflectionException If any error occurs
* @see #on(Class)
*/
public static Reflect on(String name, ClassLoader classLoader) throws ReflectionException {
return on(forName(name, classLoader));
}
/**
* Create reflector from given class type.
* Helpful especially when you want to access static fields.
*
* @param clazz Given class type
* @return Reflector
*/
public static Reflect on(Class<?> clazz) {
return new Reflect(clazz);
}
/**
* Wrap an object and return its reflector.<p>
* Helpful especially when you want to access instance fields and methods on any {@link Object}
*
* @param object The object to be wrapped
* @return Reflector
*/
public static Reflect on(Object object) {
return new Reflect(object);
}
private static Reflect on(Method method, Object receiver, Object... args) throws ReflectionException {
try {
makeAccessible(method);
if (method.getReturnType() == void.class) {
method.invoke(receiver, args);
return on(receiver);
} else {
return on(method.invoke(receiver, args));
}
} catch (Exception e) {
throw new ReflectionException(e);
}
}
/**
* Make an {@link AccessibleObject} accessible.
*
* @param accessible
* @param <T>
* @return
*/
public static <T extends AccessibleObject> T makeAccessible(T accessible) {
if (accessible == null) {
return null;
}
if (accessible instanceof Member) {
Member member = (Member) accessible;
if (Modifier.isPublic(member.getModifiers()) &&
Modifier.isPublic(member.getDeclaringClass().getModifiers())) {
return accessible;
}
}
if (!accessible.isAccessible()) {
accessible.setAccessible(true);
}
return accessible;
}
private static String property(String string) {
int length = string.length();
if (length == 0) {
return "";
} else if (length == 1) {
return string.toLowerCase();
} else {
return string.substring(0, 1).toLowerCase() + string.substring(1);
}
}
private static Reflect on(Constructor<?> constructor, Object... args) throws ReflectionException {
try {
return on(makeAccessible(constructor).newInstance(args));
} catch (Exception e) {
throw new ReflectionException(e);
}
}
/**
* If we are wrapping another reflector, get its real object.
*/
private static Object unwrap(Object object) {
if (object instanceof Reflect) {
return ((Reflect) object).get();
}
return object;
}
/**
* Convert object arrays into elements' class type arrays.
* If encountered {@code null}, use {@link NullPointer}'s class type instead.
*
* @see Object#getClass()
*/
private static Class<?>[] convertTypes(Object... values) {
if (values == null) {
return new Class[0];
}
Class<?>[] result = new Class[values.length];
for (int i = 0; i < values.length; i++) {
Object value = values[i];
result[i] = value == null ? NullPointer.class : value.getClass();
}
return result;
}
/**
* Get a class type of a class, which may cause its static-initialization
*
* @see Class#forName(String)
*/
private static Class<?> forName(String name) throws ReflectionException {
try {
return Class.forName(name);
} catch (Exception e) {
throw new ReflectionException(e);
}
}
private static Class<?> forName(String name, ClassLoader classLoader) throws ReflectionException {
try {
return Class.forName(name, true, classLoader);
} catch (Exception e) {
throw new ReflectionException(e);
}
}
/**
* Wrap primitive class types into object class types.
*
* @param type Class type that may be primitive class type
* @return Wrapped class type
*/
private static Class<?> wrapClassType(Class<?> type) {
if (type == null) {
return null;
} else if (type.isPrimitive()) {
if (boolean.class == type) {
return Boolean.class;
} else if (int.class == type) {
return Integer.class;
} else if (long.class == type) {
return Long.class;
} else if (short.class == type) {
return Short.class;
} else if (byte.class == type) {
return Byte.class;
} else if (double.class == type) {
return Double.class;
} else if (float.class == type) {
return Float.class;
} else if (char.class == type) {
return Character.class;
} else if (void.class == type) {
return Void.class;
}
}
return type;
}
/**
* Get the real object that reflector operates.
*
* @param <T> The type of the real object.
* @return The real object.
*/
@SuppressWarnings("unchecked")
public <T> T get() {
return (T) mObject;
}
/**
* Set a field to given value.
*
* @param name Field name
* @param value New value
* @return Reflector
* @throws ReflectionException If any error occurs
*/
public Reflect set(String name, Object value) throws ReflectionException {
try {
Field field = lookupField(name);
field.setAccessible(true);
field.set(mObject, unwrap(value));
return this;
} catch (Exception e) {
throw new ReflectionException(e);
}
}
/**
* Get the value of given field
*
* @param name Field name
* @param <T> The type of value
* @return Value
* @throws ReflectionException If any error occurs
*/
public <T> T get(String name) throws ReflectionException {
return field(name).get();
}
/**
* Get field by name.
*
* @param name Field name
* @return {@link Field}
* @throws ReflectionException If any error occurs
*/
public Reflect field(String name) throws ReflectionException {
try {
Field field = lookupField(name);
return on(field.get(mObject));
} catch (Exception e) {
throw new ReflectionException(e);
}
}
private Field lookupField(String name) throws ReflectionException {
Class<?> type = type();
// 先尝试取得公有字段
try {
return type.getField(name);
}
//此时尝试非公有字段
catch (NoSuchFieldException e) {
do {
try {
return makeAccessible(type.getDeclaredField(name));
} catch (NoSuchFieldException ignore) {
}
type = type.getSuperclass();
}
while (type != null);
throw new ReflectionException(e);
}
}
/**
* Load all fields into a map, the key is field name and the value is its reflector.
*
* @return Map to all fields.
*/
public Map<String, Reflect> fields() {
Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
Class<?> type = type();
do {
for (Field field : type.getDeclaredFields()) {
if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
String name = field.getName();
if (!result.containsKey(name))
result.put(name, field(name));
}
}
type = type.getSuperclass();
}
while (type != null);
return result;
}
/**
* Call a method by name without parameters.
*
* @param name Method name
* @return Reflector to the return value of the method
* @throws ReflectionException If any error occurs
*/
public Reflect call(String name) throws ReflectionException {
return call(name, new Object[0]);
}
/**
* Call a method by name and parameters.
*
* @param name Method name
* @param args Parameters
* @return Reflector to the return value of the method
* @throws ReflectionException If any error occurs
*/
public Reflect call(String name, Object... args) throws ReflectionException {
Class<?>[] types = convertTypes(args);
try {
Method method = exactMethod(name, types);
return on(method, mObject, args);
} catch (NoSuchMethodException e) {
try {
Method method = lookupSimilarMethod(name, types);
return on(method, mObject, args);
} catch (NoSuchMethodException e1) {
throw new ReflectionException(e1);
}
}
}
private Method exactMethod(String name, Class<?>[] types) throws NoSuchMethodException {
Class<?> type = type();
try {
return type.getMethod(name, types);
} catch (NoSuchMethodException e) {
do {
try {
return type.getDeclaredMethod(name, types);
} catch (NoSuchMethodException ignore) {
}
type = type.getSuperclass();
}
while (type != null);
throw new NoSuchMethodException();
}
}
/**
* Find a method that is similar to the wanted one.
*/
private Method lookupSimilarMethod(String name, Class<?>[] types) throws NoSuchMethodException {
Class<?> type = type();
for (Method method : type.getMethods()) {
if (isSignatureSimilar(method, name, types)) {
return method;
}
}
do {
for (Method method : type.getDeclaredMethods()) {
if (isSignatureSimilar(method, name, types)) {
return method;
}
}
type = type.getSuperclass();
}
while (type != null);
throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types) + " could be found on type " + type() + ".");
}
private boolean isSignatureSimilar(Method possiblyMatchingMethod,
String wantedMethodName,
Class<?>[] wantedParamTypes) {
return possiblyMatchingMethod.getName().equals(wantedMethodName)
&& match(possiblyMatchingMethod.getParameterTypes(), wantedParamTypes);
}
/**
* Create an instance using its default constructor.
*
* @return Reflector to the return value of the method
* @throws ReflectionException If any error occurs
*/
public Reflect create() throws ReflectionException {
return create(new Object[0]);
}
/**
* Create an instance by parameters.
*
* @param args Parameters
* @return Reflector to the return value of the method
* @throws ReflectionException If any error occurs
*/
public Reflect create(Object... args) throws ReflectionException {
Class<?>[] types = convertTypes(args);
try {
Constructor<?> constructor = type().getDeclaredConstructor(types);
return on(constructor, args);
} catch (NoSuchMethodException e) {
for (Constructor<?> constructor : type().getDeclaredConstructors()) {
if (match(constructor.getParameterTypes(), types)) {
return on(constructor, args);
}
}
throw new ReflectionException(e);
}
}
/**
* Create a dynamic proxy based on the given type.
* If we are maintaining a Map and error occurs when calling methods,
* we will return value from Map as return value.
* Helpful especially when creating default data handlers.
*
* @param proxyType The type to be proxy-ed
* @return Proxy object
*/
@SuppressWarnings("unchecked")
public <P> P as(Class<P> proxyType) {
final boolean isMap = (mObject instanceof Map);
final InvocationHandler handler = (proxy, method, args) -> {
String name = method.getName();
try {
return on(mObject).call(name, args).get();
} catch (ReflectionException e) {
if (isMap) {
Map<String, Object> map = (Map<String, Object>) mObject;
int length = (args == null ? 0 : args.length);
// Pay special attention to those getters and setters
if (length == 0 && name.startsWith("get")) {
return map.get(property(name.substring(3)));
} else if (length == 0 && name.startsWith("is")) {
return map.get(property(name.substring(2)));
} else if (length == 1 && name.startsWith("set")) {
map.put(property(name.substring(3)), args[0]);
return null;
}
}
throw e;
}
};
return (P) Proxy.newProxyInstance(proxyType.getClassLoader(),
new Class[]{proxyType}, handler);
}
/**
* Check whether types matches to avoid {@link ClassCastException} when calling a method.
* If encountered primitive type, convert to object type first.
*/
private boolean match(Class<?>[] declaredTypes, Class<?>[] actualTypes) {
if (declaredTypes.length == actualTypes.length) {
for (int i = 0; i < actualTypes.length; i++) {
// nulls are acceptable on any occasions
if (actualTypes[i] == NullPointer.class) {
continue;
}
if (wrapClassType(declaredTypes[i]).isAssignableFrom(wrapClassType(actualTypes[i]))) {
continue;
}
return false;
}
return true;
} else {
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return mObject.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof Reflect) {
return mObject.equals(((Reflect) obj).get());
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return mObject.toString();
}
/**
* Get the class type of the real object that reflector operates.
*
* @see Object#getClass()
*/
public Class<?> type() {
if (isClass) {
return (Class<?>) mObject;
} else {
return mObject.getClass();
}
}
}

View File

@ -0,0 +1,10 @@
package io.neoterm.framework.reflection;
/**
* @author kiva
*/
public class ReflectionException extends RuntimeException {
ReflectionException(Throwable cause) {
super(cause);
}
}

View File

@ -24,7 +24,7 @@ object NeoPreference {
const val KEY_FONT_SIZE = "neoterm_general_font_size" const val KEY_FONT_SIZE = "neoterm_general_font_size"
const val KEY_CURRENT_SESSION = "neoterm_service_current_session" const val KEY_CURRENT_SESSION = "neoterm_service_current_session"
const val KEY_SYSTEM_SHELL = "neoterm_core_system_shell" const val KEY_SYSTEM_SHELL = "neoterm_core_system_shell"
const val KEY_SOURCES = "neoterm_source_source_list" const val KEY_SOURCES = "neoterm_package_enabled_sources"
const val VALUE_HAPPY_EGG_TRIGGER = 8 const val VALUE_HAPPY_EGG_TRIGGER = 8
@ -56,14 +56,6 @@ object NeoPreference {
} }
} }
fun storeStrings(key: String, value: Set<String>) {
preference!!.edit().putStringSet(key, value).apply()
}
fun loadStrings(key: String): Set<String> {
return preference!!.getStringSet(key, setOf())
}
fun store(key: Int, value: Any) { fun store(key: Int, value: Any) {
store(App.get().getString(key), value) store(App.get().getString(key), value)
} }

View File

@ -27,9 +27,9 @@ object NeoTermPath {
private const val SOURCE = "http://janyo.pw:82/kiva/neoterm" private const val SOURCE = "http://janyo.pw:82/kiva/neoterm"
val DEFAULT_SOURCE: String val DEFAULT_MAIN_PACKAGE_SOURCE: String
init { init {
DEFAULT_SOURCE = SOURCE DEFAULT_MAIN_PACKAGE_SOURCE = SOURCE
} }
} }

View File

@ -96,10 +96,10 @@ class ColorSchemeActivity : BaseCustomizeActivity() {
} }
private fun showItemEditor(model: ColorItem) { private fun showItemEditor(model: ColorItem) {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_color, null, false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null, false)
view.findViewById<TextView>(R.id.dialog_edit_color_info).text = getString(R.string.input_new_value) view.findViewById<TextView>(R.id.dialog_edit_text_info).text = getString(R.string.input_new_value)
val edit = view.findViewById<EditText>(R.id.dialog_edit_color_editor) val edit = view.findViewById<EditText>(R.id.dialog_edit_text_editor)
edit.setText(model.colorValue) edit.setText(model.colorValue)
if (model.colorValue.isNotEmpty()) { if (model.colorValue.isNotEmpty()) {
edit.setTextColor(TerminalColors.parse(model.colorValue)) edit.setTextColor(TerminalColors.parse(model.colorValue))

View File

@ -1,5 +1,6 @@
package io.neoterm.ui.pm package io.neoterm.ui.pm
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.support.v4.view.MenuItemCompat import android.support.v4.view.MenuItemCompat
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
@ -8,16 +9,19 @@ import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.EditText import android.widget.EditText
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter import com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter
import io.neoterm.R import io.neoterm.R
import io.neoterm.backend.TerminalSession import io.neoterm.backend.TerminalSession
import io.neoterm.component.pm.PackageComponent import io.neoterm.component.pm.PackageComponent
import io.neoterm.component.pm.Source
import io.neoterm.component.pm.SourceManager import io.neoterm.component.pm.SourceManager
import io.neoterm.component.pm.SourceUtils import io.neoterm.component.pm.SourceHelper
import io.neoterm.frontend.component.ComponentManager import io.neoterm.frontend.component.ComponentManager
import io.neoterm.frontend.config.NeoPreference import io.neoterm.frontend.config.NeoPreference
import io.neoterm.frontend.config.NeoTermPath import io.neoterm.frontend.config.NeoTermPath
@ -109,24 +113,16 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
private fun changeSource() { private fun changeSource() {
val sourceManager = ComponentManager.getComponent<PackageComponent>().sourceManager val sourceManager = ComponentManager.getComponent<PackageComponent>().sourceManager
val sourceList = sourceManager.sources val sourceList = sourceManager.getAllSources()
val currentSource = NeoPreference.loadString(R.string.key_package_source, NeoTermPath.DEFAULT_SOURCE)
var checkedItem = sourceList.indexOf(currentSource)
if (checkedItem == -1) {
// Users may edit source.list on his own
checkedItem = sourceList.size
sourceManager.addSource(currentSource)
}
var selectedIndex = 0
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.pref_package_source) .setTitle(R.string.pref_package_source)
.setSingleChoiceItems(sourceList.toTypedArray(), checkedItem, { _, which -> .setMultiChoiceItems(sourceList.map { "${it.url} :: ${it.repo}" }.toTypedArray(),
selectedIndex = which sourceList.map { it.enabled }.toBooleanArray(), { dialog, which, isChecked ->
sourceList[which].enabled = isChecked
}) })
.setPositiveButton(android.R.string.yes, { _, _ -> .setPositiveButton(android.R.string.yes, { _, _ ->
changeSourceInternal(sourceManager, sourceList.elementAt(selectedIndex)) changeSourceInternal(sourceManager, sourceList)
}) })
.setNeutralButton(R.string.new_source, { _, _ -> .setNeutralButton(R.string.new_source, { _, _ ->
changeSourceToUserInput(sourceManager) changeSourceToUserInput(sourceManager)
@ -135,26 +131,51 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
.show() .show()
} }
@SuppressLint("SetTextI18n")
private fun changeSourceToUserInput(sourceManager: SourceManager) { private fun changeSourceToUserInput(sourceManager: SourceManager) {
val editText = EditText(this) val view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_two_text, null, false)
editText.setSelectAllOnFocus(true) 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) AlertDialog.Builder(this)
.setTitle(R.string.pref_package_source) .setTitle(R.string.pref_package_source)
.setView(editText) .setView(view)
.setNegativeButton(android.R.string.no, null) .setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, { _, _ -> .setPositiveButton(android.R.string.yes, { _, _ ->
val source = editText.text.toString() val url = urlEditor.text.toString()
changeSourceInternal(sourceManager, source) 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() .show()
} }
private fun changeSourceInternal(sourceManager: SourceManager, source: String) { private fun changeSourceInternal(sourceManager: SourceManager, source: List<Source>) {
sourceManager.addSource(source) sourceManager.updateAll(source)
postChangeSource(sourceManager)
}
private fun postChangeSource(sourceManager: SourceManager) {
sourceManager.applyChanges() sourceManager.applyChanges()
NeoPreference.store(R.string.key_package_source, source) NeoPreference.store(R.string.key_package_source, sourceManager.getMainPackageSource())
PackageUtils.syncSource() SourceHelper.syncSource(sourceManager)
executeAptUpdate() executeAptUpdate()
} }
@ -193,17 +214,11 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
models.clear() models.clear()
Thread { Thread {
val pm = ComponentManager.getComponent<PackageComponent>() val pm = ComponentManager.getComponent<PackageComponent>()
val sourceFiles = SourceUtils.detectSourceFiles() val sourceFiles = SourceHelper.detectSourceFiles()
pm.clearPackages() pm.clearPackages()
for (index in sourceFiles.indices) { sourceFiles.forEach { pm.reloadPackages(it, false) }
pm.reloadPackages(sourceFiles[index], false) pm.packages.values.mapTo(models, { PackageModel(it) })
}
val packages = pm.packages
for (packageInfo in packages.values) {
models.add(PackageModel(packageInfo))
}
this@PackageManagerActivity.runOnUiThread { this@PackageManagerActivity.runOnUiThread {
adapter.edit() adapter.edit()

View File

@ -10,6 +10,7 @@ import android.view.View
import android.widget.* import android.widget.*
import io.neoterm.App import io.neoterm.App
import io.neoterm.R import io.neoterm.R
import io.neoterm.component.pm.SourceHelper
import io.neoterm.frontend.config.NeoTermPath import io.neoterm.frontend.config.NeoTermPath
import io.neoterm.setup.ResultListener import io.neoterm.setup.ResultListener
import io.neoterm.setup.SetupHelper import io.neoterm.setup.SetupHelper
@ -183,7 +184,7 @@ class SetupActivity : AppCompatActivity(), View.OnClickListener, ResultListener
private fun setDefaultValue(parameterEditor: EditText, id: Int) { private fun setDefaultValue(parameterEditor: EditText, id: Int) {
setupParameter = when (id) { setupParameter = when (id) {
R.id.setup_method_online -> NeoTermPath.DEFAULT_SOURCE R.id.setup_method_online -> NeoTermPath.DEFAULT_MAIN_PACKAGE_SOURCE
else -> "" else -> ""
} }
parameterEditor.setText(setupParameter) parameterEditor.setText(setupParameter)
@ -211,7 +212,7 @@ class SetupActivity : AppCompatActivity(), View.OnClickListener, ResultListener
override fun onResult(error: Exception?) { override fun onResult(error: Exception?) {
if (error == null) { if (error == null) {
setResult(RESULT_OK) setResult(RESULT_OK)
PackageUtils.syncSource() SourceHelper.syncSource()
executeAptUpdate() executeAptUpdate()
} else { } else {

View File

@ -1,9 +1,10 @@
package io.neoterm.utils package io.neoterm.utils
import android.content.Context import android.content.Context
import io.neoterm.R
import io.neoterm.backend.TerminalSession import io.neoterm.backend.TerminalSession
import io.neoterm.frontend.config.NeoPreference import io.neoterm.component.pm.PackageComponent
import io.neoterm.component.pm.SourceManager
import io.neoterm.frontend.component.ComponentManager
import io.neoterm.frontend.config.NeoTermPath import io.neoterm.frontend.config.NeoTermPath
import io.neoterm.frontend.floating.TerminalDialog import io.neoterm.frontend.floating.TerminalDialog
import java.io.File import java.io.File
@ -12,21 +13,6 @@ import java.io.File
* @author kiva * @author kiva
*/ */
object PackageUtils { object PackageUtils {
fun syncSource() {
val source = NeoPreference.loadString(R.string.key_package_source, NeoTermPath.DEFAULT_SOURCE)
val sourceFile = File(NeoTermPath.SOURCE_FILE)
FileUtils.writeFile(sourceFile, generateSourceFile(source).toByteArray())
}
private fun generateSourceFile(source: String): String {
return StringBuilder().append("# Generated by NeoTerm-Preference\n")
.append("deb ")
.append(source)
.append(" stable main")
.append("\n")
.toString()
}
fun apt(context: Context, subCommand: String, extraArgs: Array<String>?, callback: (Int, TerminalDialog) -> Unit) { fun apt(context: Context, subCommand: String, extraArgs: Array<String>?, callback: (Int, TerminalDialog) -> Unit) {
val argArray = val argArray =
if (extraArgs != null) arrayOf(NeoTermPath.APT_BIN_PATH, subCommand, *extraArgs) if (extraArgs != null) arrayOf(NeoTermPath.APT_BIN_PATH, subCommand, *extraArgs)

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/text_margin">
<TextView
android:id="@+id/dialog_edit_color_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<EditText
android:id="@+id/dialog_edit_color_editor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="text"
android:maxLines="1" />
</LinearLayout>

View File

@ -6,6 +6,7 @@
android:padding="@dimen/text_margin"> android:padding="@dimen/text_margin">
<TextView <TextView
android:labelFor="@id/dialog_edit_text_editor"
android:id="@+id/dialog_edit_text_info" android:id="@+id/dialog_edit_text_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/text_margin">
<TextView
android:id="@+id/dialog_edit_text_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@id/dialog_edit_text_editor"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<EditText
android:id="@+id/dialog_edit_text_editor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="text"
android:maxLines="1" />
<TextView
android:id="@+id/dialog_edit_text2_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@id/dialog_edit_text2_editor"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<EditText
android:id="@+id/dialog_edit_text2_editor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="text"
android:maxLines="1" />
</LinearLayout>

View File

@ -157,6 +157,10 @@
<string name="new_session_with_profile">新建 Profile 会话</string> <string name="new_session_with_profile">新建 Profile 会话</string>
<string name="no_profile_available">没有可用的个性化配置</string> <string name="no_profile_available">没有可用的个性化配置</string>
<string name="no_file_picker">没有文件选择器</string> <string name="no_file_picker">没有文件选择器</string>
<string name="input_new_source_url">输入 URL</string>
<string name="input_new_source_repo">输入仓库名</string>
<string name="error_new_source_url">URL 不能为空</string>
<string name="error_new_source_repo">仓库 不能为空</string>
<string-array name="color_item_names"> <string-array name="color_item_names">
<item>背景色</item> <item>背景色</item>

View File

@ -161,6 +161,10 @@
<string name="new_session_with_profile">New Session With Profile</string> <string name="new_session_with_profile">New Session With Profile</string>
<string name="no_profile_available">No profile available</string> <string name="no_profile_available">No profile available</string>
<string name="no_file_picker">No file picker found</string> <string name="no_file_picker">No file picker found</string>
<string name="input_new_source_url">Enter new URL</string>
<string name="input_new_source_repo">Enter new Repo</string>
<string name="error_new_source_url">URL cannot be empty</string>
<string name="error_new_source_repo">Repo cannot be empty</string>
<string name="default_source_url">http://janyo.pw:82/kiva/neoterm</string> <string name="default_source_url">http://janyo.pw:82/kiva/neoterm</string>

View File

@ -1,10 +1,9 @@
package io.neoterm package io.neoterm
import io.neoterm.component.pm.PackageComponent import io.neoterm.component.pm.PackageComponent
import io.neoterm.component.pm.SourceUtils import io.neoterm.component.pm.SourceHelper
import io.neoterm.frontend.component.ComponentManager import io.neoterm.frontend.component.ComponentManager
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
@ -15,7 +14,7 @@ class PackageManagerTest {
@Test @Test
fun testSourceUrl() { fun testSourceUrl() {
val url = "http://7sp0th.iok.la:81/neoterm" val url = "http://7sp0th.iok.la:81/neoterm"
println(SourceUtils.detectSourceFilePrefix(url)) println(SourceHelper.detectSourceFilePrefix(url))
} }
@Test @Test