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

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

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

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

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

@ -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;
}
}

@ -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 ""
}
}
}

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

@ -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 ""
}
}
}

@ -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();
}
}
}
}

@ -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;
}
}

@ -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;
}
}

@ -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);
}

@ -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;
}
}

@ -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;
}
}

@ -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();
}
}

@ -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;
}
}

@ -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;
}

@ -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 {
}

@ -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 {
}

@ -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 "";
}

@ -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;
}

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

@ -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();
}
}
}

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

@ -24,7 +24,7 @@ object NeoPreference {
const val KEY_FONT_SIZE = "neoterm_general_font_size"
const val KEY_CURRENT_SESSION = "neoterm_service_current_session"
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
@ -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) {
store(App.get().getString(key), value)
}

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

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

@ -1,5 +1,6 @@
package io.neoterm.ui.pm
import android.annotation.SuppressLint
import android.os.Bundle
import android.support.v4.view.MenuItemCompat
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.SearchView
import android.support.v7.widget.Toolbar
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 com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.component.pm.PackageComponent
import io.neoterm.component.pm.Source
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.config.NeoPreference
import io.neoterm.frontend.config.NeoTermPath
@ -109,24 +113,16 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
private fun changeSource() {
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)
.setTitle(R.string.pref_package_source)
.setSingleChoiceItems(sourceList.toTypedArray(), checkedItem, { _, which ->
selectedIndex = which
.setMultiChoiceItems(sourceList.map { "${it.url} :: ${it.repo}" }.toTypedArray(),
sourceList.map { it.enabled }.toBooleanArray(), { dialog, which, isChecked ->
sourceList[which].enabled = isChecked
})
.setPositiveButton(android.R.string.yes, { _, _ ->
changeSourceInternal(sourceManager, sourceList.elementAt(selectedIndex))
changeSourceInternal(sourceManager, sourceList)
})
.setNeutralButton(R.string.new_source, { _, _ ->
changeSourceToUserInput(sourceManager)
@ -135,26 +131,51 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
.show()
}
@SuppressLint("SetTextI18n")
private fun changeSourceToUserInput(sourceManager: SourceManager) {
val editText = EditText(this)
editText.setSelectAllOnFocus(true)
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(editText)
.setView(view)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, { _, _ ->
val source = editText.text.toString()
changeSourceInternal(sourceManager, source)
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: String) {
sourceManager.addSource(source)
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, source)
PackageUtils.syncSource()
NeoPreference.store(R.string.key_package_source, sourceManager.getMainPackageSource())
SourceHelper.syncSource(sourceManager)
executeAptUpdate()
}
@ -193,17 +214,11 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
models.clear()
Thread {
val pm = ComponentManager.getComponent<PackageComponent>()
val sourceFiles = SourceUtils.detectSourceFiles()
val sourceFiles = SourceHelper.detectSourceFiles()
pm.clearPackages()
for (index in sourceFiles.indices) {
pm.reloadPackages(sourceFiles[index], false)
}
val packages = pm.packages
for (packageInfo in packages.values) {
models.add(PackageModel(packageInfo))
}
sourceFiles.forEach { pm.reloadPackages(it, false) }
pm.packages.values.mapTo(models, { PackageModel(it) })
this@PackageManagerActivity.runOnUiThread {
adapter.edit()

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

@ -1,9 +1,10 @@
package io.neoterm.utils
import android.content.Context
import io.neoterm.R
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.floating.TerminalDialog
import java.io.File
@ -12,21 +13,6 @@ import java.io.File
* @author kiva
*/
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) {
val argArray =
if (extraArgs != null) arrayOf(NeoTermPath.APT_BIN_PATH, subCommand, *extraArgs)

@ -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>

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

@ -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>

@ -157,6 +157,10 @@
<string name="new_session_with_profile">新建 Profile 会话</string>
<string name="no_profile_available">没有可用的个性化配置</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">
<item>背景色</item>

@ -161,6 +161,10 @@
<string name="new_session_with_profile">New Session With Profile</string>
<string name="no_profile_available">No profile available</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>

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