From 23937764f998203b3aab7e99c5eedafbe86f3cc8 Mon Sep 17 00:00:00 2001 From: imkiva Date: Thu, 15 Feb 2018 23:44:38 +0800 Subject: [PATCH] PackageManager: Support multi-source --- app/build.gradle | 4 + app/proguard-rules.pro | 3 + .../io/neoterm/component/pm/Architecture.kt | 14 +- .../component/pm/PackageComponent.java | 2 +- .../java/io/neoterm/component/pm/Source.java | 29 + .../io/neoterm/component/pm/SourceHelper.kt | 73 ++ .../io/neoterm/component/pm/SourceManager.kt | 43 +- .../io/neoterm/component/pm/SourceUtils.kt | 57 -- .../io/neoterm/framework/NeoTermDatabase.java | 675 ++++++++++++++++++ .../framework/database/DatabaseDataType.java | 38 + .../database/NeoTermSQLiteConfig.java | 123 ++++ .../database/OnDatabaseUpgradedListener.java | 15 + .../database/SQLStatementHelper.java | 198 +++++ .../framework/database/SQLTypeParser.java | 81 +++ .../framework/database/TableHelper.java | 132 ++++ .../framework/database/ValueHelper.java | 118 +++ .../framework/database/annotation/ID.java | 20 + .../framework/database/annotation/Ignore.java | 14 + .../database/annotation/NotNull.java | 14 + .../framework/database/annotation/Table.java | 23 + .../framework/database/bean/TableInfo.java | 46 ++ .../framework/reflection/NullPointer.java | 8 + .../neoterm/framework/reflection/Reflect.java | 560 +++++++++++++++ .../reflection/ReflectionException.java | 10 + .../neoterm/frontend/config/NeoPreference.kt | 10 +- .../io/neoterm/frontend/config/NeoTermPath.kt | 4 +- .../ui/customize/ColorSchemeActivity.kt | 6 +- .../neoterm/ui/pm/PackageManagerActivity.kt | 79 +- .../java/io/neoterm/ui/setup/SetupActivity.kt | 5 +- .../java/io/neoterm/utils/PackageUtils.kt | 20 +- app/src/main/res/layout/dialog_edit_color.xml | 21 - app/src/main/res/layout/dialog_edit_text.xml | 1 + .../main/res/layout/dialog_edit_two_text.xml | 36 + app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + .../java/io/neoterm/PackageManagerTest.kt | 5 +- 36 files changed, 2332 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/io/neoterm/component/pm/Source.java create mode 100644 app/src/main/java/io/neoterm/component/pm/SourceHelper.kt delete mode 100644 app/src/main/java/io/neoterm/component/pm/SourceUtils.kt create mode 100644 app/src/main/java/io/neoterm/framework/NeoTermDatabase.java create mode 100644 app/src/main/java/io/neoterm/framework/database/DatabaseDataType.java create mode 100644 app/src/main/java/io/neoterm/framework/database/NeoTermSQLiteConfig.java create mode 100644 app/src/main/java/io/neoterm/framework/database/OnDatabaseUpgradedListener.java create mode 100644 app/src/main/java/io/neoterm/framework/database/SQLStatementHelper.java create mode 100644 app/src/main/java/io/neoterm/framework/database/SQLTypeParser.java create mode 100644 app/src/main/java/io/neoterm/framework/database/TableHelper.java create mode 100644 app/src/main/java/io/neoterm/framework/database/ValueHelper.java create mode 100644 app/src/main/java/io/neoterm/framework/database/annotation/ID.java create mode 100644 app/src/main/java/io/neoterm/framework/database/annotation/Ignore.java create mode 100644 app/src/main/java/io/neoterm/framework/database/annotation/NotNull.java create mode 100644 app/src/main/java/io/neoterm/framework/database/annotation/Table.java create mode 100644 app/src/main/java/io/neoterm/framework/database/bean/TableInfo.java create mode 100644 app/src/main/java/io/neoterm/framework/reflection/NullPointer.java create mode 100644 app/src/main/java/io/neoterm/framework/reflection/Reflect.java create mode 100644 app/src/main/java/io/neoterm/framework/reflection/ReflectionException.java delete mode 100644 app/src/main/res/layout/dialog_edit_color.xml create mode 100644 app/src/main/res/layout/dialog_edit_two_text.xml diff --git a/app/build.gradle b/app/build.gradle index f881157..ac0bb64 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,6 +46,10 @@ android { abortOnError false checkReleaseBuilds false } + compileOptions { + targetCompatibility 1.8 + sourceCompatibility 1.8 + } } dependencies { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 5a1dafd..5297564 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -94,3 +94,6 @@ java.lang.Object readResolve(); } +-keep class * extends io.neoterm.framework.database.annotation.* { *; } +-keep interface * extends io.neoterm.framework.database.annotation.* { *; } + diff --git a/app/src/main/java/io/neoterm/component/pm/Architecture.kt b/app/src/main/java/io/neoterm/component/pm/Architecture.kt index 1e092ef..6e5351f 100644 --- a/app/src/main/java/io/neoterm/component/pm/Architecture.kt +++ b/app/src/main/java/io/neoterm/component/pm/Architecture.kt @@ -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 } } } diff --git a/app/src/main/java/io/neoterm/component/pm/PackageComponent.java b/app/src/main/java/io/neoterm/component/pm/PackageComponent.java index 5d828e9..2693093 100644 --- a/app/src/main/java/io/neoterm/component/pm/PackageComponent.java +++ b/app/src/main/java/io/neoterm/component/pm/PackageComponent.java @@ -22,7 +22,7 @@ public class PackageComponent implements NeoComponent { } public HashMap getPackages() { - return queryEnabled ? neoPackages : new HashMap(); + return queryEnabled ? neoPackages : new HashMap<>(); } public int getPackageCount() { diff --git a/app/src/main/java/io/neoterm/component/pm/Source.java b/app/src/main/java/io/neoterm/component/pm/Source.java new file mode 100644 index 0000000..68520be --- /dev/null +++ b/app/src/main/java/io/neoterm/component/pm/Source.java @@ -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; + } +} diff --git a/app/src/main/java/io/neoterm/component/pm/SourceHelper.kt b/app/src/main/java/io/neoterm/component/pm/SourceHelper.kt new file mode 100644 index 0000000..1edffb6 --- /dev/null +++ b/app/src/main/java/io/neoterm/component/pm/SourceHelper.kt @@ -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().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 { + val sourceManager = ComponentManager.getComponent().sourceManager + val sourceFiles = ArrayList() + 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 "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/neoterm/component/pm/SourceManager.kt b/app/src/main/java/io/neoterm/component/pm/SourceManager.kt index 127aab5..270efb4 100644 --- a/app/src/main/java/io/neoterm/component/pm/SourceManager.kt +++ b/app/src/main/java/io/neoterm/component/pm/SourceManager.kt @@ -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() + 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::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) { + database.dropAllTable() + database.saveBeans(sources) + } + + fun getAllSources(): List { + return database.findAll(Source::class.java) + } + + fun getEnabledSources(): List { + 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() } } diff --git a/app/src/main/java/io/neoterm/component/pm/SourceUtils.kt b/app/src/main/java/io/neoterm/component/pm/SourceUtils.kt deleted file mode 100644 index 513e6b9..0000000 --- a/app/src/main/java/io/neoterm/component/pm/SourceUtils.kt +++ /dev/null @@ -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 { - val sourceFiles = ArrayList() - 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 "" - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/neoterm/framework/NeoTermDatabase.java b/app/src/main/java/io/neoterm/framework/NeoTermDatabase.java new file mode 100644 index 0000000..f259f09 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/NeoTermDatabase.java @@ -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 + *

+ * 基于DTO(DataToObject)映射的数据库操纵模型. + * 通过少量可选的注解,即可构造数据模型. + * 增删查改异常轻松. + * @version 1.4 + */ +public class NeoTermDatabase { + + /** + * 缓存创建的数据库,以便防止数据库冲突. + */ + private static final Map 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 getTableList() { + try (Cursor cursor = db.rawQuery( + "SELECT name FROM sqlite_master WHERE type ='table'", null)) { + List 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 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 NeoTermDatabase saveBeans(List beans) { + for (Object o : beans) { + saveBean(o); + } + + return this; + } + + /** + * 寻找Bean对应的全部数据 + * + * @param clazz + * @param + * @return + */ + public List findAll(Class clazz) { + createTableIfNeed(clazz); + TableInfo tableInfo = TableHelper.from(clazz); + String statement = SQLStatementHelper.selectTable(tableInfo.tableName); + if (neoTermSQLiteConfig.debugMode) { + NLog.INSTANCE.w(statement); + } + List 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; + } + } + + /** + * 根据where语句寻找Bean + * + * @param clazz + * @param + * @return + */ + public List 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 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 findOneBeanByWhere(Class clazz, String where) { + List 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 + * @return + */ + public 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(); + } + } + } +} diff --git a/app/src/main/java/io/neoterm/framework/database/DatabaseDataType.java b/app/src/main/java/io/neoterm/framework/database/DatabaseDataType.java new file mode 100644 index 0000000..2915468 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/DatabaseDataType.java @@ -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; + } + +} diff --git a/app/src/main/java/io/neoterm/framework/database/NeoTermSQLiteConfig.java b/app/src/main/java/io/neoterm/framework/database/NeoTermSQLiteConfig.java new file mode 100644 index 0000000..3b832ac --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/NeoTermSQLiteConfig.java @@ -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; + } +} diff --git a/app/src/main/java/io/neoterm/framework/database/OnDatabaseUpgradedListener.java b/app/src/main/java/io/neoterm/framework/database/OnDatabaseUpgradedListener.java new file mode 100644 index 0000000..bf600a3 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/OnDatabaseUpgradedListener.java @@ -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); +} \ No newline at end of file diff --git a/app/src/main/java/io/neoterm/framework/database/SQLStatementHelper.java b/app/src/main/java/io/neoterm/framework/database/SQLStatementHelper.java new file mode 100644 index 0000000..5d5edd8 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/SQLStatementHelper.java @@ -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 { + + /** + * 构造创建表的语句 + * + * @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; + } + +} diff --git a/app/src/main/java/io/neoterm/framework/database/SQLTypeParser.java b/app/src/main/java/io/neoterm/framework/database/SQLTypeParser.java new file mode 100644 index 0000000..10410d7 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/SQLTypeParser.java @@ -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; + } +} diff --git a/app/src/main/java/io/neoterm/framework/database/TableHelper.java b/app/src/main/java/io/neoterm/framework/database/TableHelper.java new file mode 100644 index 0000000..d352a18 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/TableHelper.java @@ -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, 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 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(); + } + +} + + diff --git a/app/src/main/java/io/neoterm/framework/database/ValueHelper.java b/app/src/main/java/io/neoterm/framework/database/ValueHelper.java new file mode 100644 index 0000000..14b34b2 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/ValueHelper.java @@ -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; + } +} diff --git a/app/src/main/java/io/neoterm/framework/database/annotation/ID.java b/app/src/main/java/io/neoterm/framework/database/annotation/ID.java new file mode 100644 index 0000000..9c157e1 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/annotation/ID.java @@ -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; +} diff --git a/app/src/main/java/io/neoterm/framework/database/annotation/Ignore.java b/app/src/main/java/io/neoterm/framework/database/annotation/Ignore.java new file mode 100644 index 0000000..56c211a --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/annotation/Ignore.java @@ -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 { +} diff --git a/app/src/main/java/io/neoterm/framework/database/annotation/NotNull.java b/app/src/main/java/io/neoterm/framework/database/annotation/NotNull.java new file mode 100644 index 0000000..b24a64a --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/annotation/NotNull.java @@ -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 { +} diff --git a/app/src/main/java/io/neoterm/framework/database/annotation/Table.java b/app/src/main/java/io/neoterm/framework/database/annotation/Table.java new file mode 100644 index 0000000..c8869e0 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/annotation/Table.java @@ -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 ""; +} diff --git a/app/src/main/java/io/neoterm/framework/database/bean/TableInfo.java b/app/src/main/java/io/neoterm/framework/database/bean/TableInfo.java new file mode 100644 index 0000000..8547a24 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/database/bean/TableInfo.java @@ -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 fieldToDataTypeMap; + + /** + * 创建table的语句 + */ + public String createTableStatement; + + /** + * 是否已经创建 + */ + public boolean isCreate = false; + + public Method afterTableCreateMethod; + +} diff --git a/app/src/main/java/io/neoterm/framework/reflection/NullPointer.java b/app/src/main/java/io/neoterm/framework/reflection/NullPointer.java new file mode 100644 index 0000000..6a88d86 --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/reflection/NullPointer.java @@ -0,0 +1,8 @@ +package io.neoterm.framework.reflection; + +/** + * class representing null pointer. + * @author kiva + */ +public class NullPointer { +} \ No newline at end of file diff --git a/app/src/main/java/io/neoterm/framework/reflection/Reflect.java b/app/src/main/java/io/neoterm/framework/reflection/Reflect.java new file mode 100644 index 0000000..c3494ad --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/reflection/Reflect.java @@ -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.

+ * 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 + * @return + */ + public static 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 The type of the real object. + * @return The real object. + */ + @SuppressWarnings("unchecked") + public 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 The type of value + * @return Value + * @throws ReflectionException If any error occurs + */ + public 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 fields() { + Map result = new LinkedHashMap(); + 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 as(Class

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 map = (Map) 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(); + } + } +} diff --git a/app/src/main/java/io/neoterm/framework/reflection/ReflectionException.java b/app/src/main/java/io/neoterm/framework/reflection/ReflectionException.java new file mode 100644 index 0000000..ee8241f --- /dev/null +++ b/app/src/main/java/io/neoterm/framework/reflection/ReflectionException.java @@ -0,0 +1,10 @@ +package io.neoterm.framework.reflection; + +/** + * @author kiva + */ +public class ReflectionException extends RuntimeException { + ReflectionException(Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/io/neoterm/frontend/config/NeoPreference.kt b/app/src/main/java/io/neoterm/frontend/config/NeoPreference.kt index 08ce026..5aa2428 100644 --- a/app/src/main/java/io/neoterm/frontend/config/NeoPreference.kt +++ b/app/src/main/java/io/neoterm/frontend/config/NeoPreference.kt @@ -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) { - preference!!.edit().putStringSet(key, value).apply() - } - - fun loadStrings(key: String): Set { - return preference!!.getStringSet(key, setOf()) - } - fun store(key: Int, value: Any) { store(App.get().getString(key), value) } diff --git a/app/src/main/java/io/neoterm/frontend/config/NeoTermPath.kt b/app/src/main/java/io/neoterm/frontend/config/NeoTermPath.kt index b429ff1..51058ff 100644 --- a/app/src/main/java/io/neoterm/frontend/config/NeoTermPath.kt +++ b/app/src/main/java/io/neoterm/frontend/config/NeoTermPath.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/io/neoterm/ui/customize/ColorSchemeActivity.kt b/app/src/main/java/io/neoterm/ui/customize/ColorSchemeActivity.kt index 5bc4f0e..9e1c1cd 100644 --- a/app/src/main/java/io/neoterm/ui/customize/ColorSchemeActivity.kt +++ b/app/src/main/java/io/neoterm/ui/customize/ColorSchemeActivity.kt @@ -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(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(R.id.dialog_edit_text_info).text = getString(R.string.input_new_value) - val edit = view.findViewById(R.id.dialog_edit_color_editor) + val edit = view.findViewById(R.id.dialog_edit_text_editor) edit.setText(model.colorValue) if (model.colorValue.isNotEmpty()) { edit.setTextColor(TerminalColors.parse(model.colorValue)) diff --git a/app/src/main/java/io/neoterm/ui/pm/PackageManagerActivity.kt b/app/src/main/java/io/neoterm/ui/pm/PackageManagerActivity.kt index a03813e..4e4ddde 100644 --- a/app/src/main/java/io/neoterm/ui/pm/PackageManagerActivity.kt +++ b/app/src/main/java/io/neoterm/ui/pm/PackageManagerActivity.kt @@ -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().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(R.id.dialog_edit_text_info).text = getString(R.string.input_new_source_url) + view.findViewById(R.id.dialog_edit_text2_info).text = getString(R.string.input_new_source_repo) + + val urlEditor = view.findViewById(R.id.dialog_edit_text_editor) + val repoEditor = view.findViewById(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) { + 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() - 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() diff --git a/app/src/main/java/io/neoterm/ui/setup/SetupActivity.kt b/app/src/main/java/io/neoterm/ui/setup/SetupActivity.kt index 925f14d..0dc57c8 100644 --- a/app/src/main/java/io/neoterm/ui/setup/SetupActivity.kt +++ b/app/src/main/java/io/neoterm/ui/setup/SetupActivity.kt @@ -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 { diff --git a/app/src/main/java/io/neoterm/utils/PackageUtils.kt b/app/src/main/java/io/neoterm/utils/PackageUtils.kt index 2b14c7e..36c2403 100644 --- a/app/src/main/java/io/neoterm/utils/PackageUtils.kt +++ b/app/src/main/java/io/neoterm/utils/PackageUtils.kt @@ -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?, callback: (Int, TerminalDialog) -> Unit) { val argArray = if (extraArgs != null) arrayOf(NeoTermPath.APT_BIN_PATH, subCommand, *extraArgs) diff --git a/app/src/main/res/layout/dialog_edit_color.xml b/app/src/main/res/layout/dialog_edit_color.xml deleted file mode 100644 index 05dc237..0000000 --- a/app/src/main/res/layout/dialog_edit_color.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_text.xml b/app/src/main/res/layout/dialog_edit_text.xml index bb9aa99..3fef9ea 100644 --- a/app/src/main/res/layout/dialog_edit_text.xml +++ b/app/src/main/res/layout/dialog_edit_text.xml @@ -6,6 +6,7 @@ android:padding="@dimen/text_margin"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ec4c970..e9c44b1 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -157,6 +157,10 @@ 新建 Profile 会话 没有可用的个性化配置 没有文件选择器 + 输入 URL + 输入仓库名 + URL 不能为空 + 仓库 不能为空 背景色 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fa10e0..3f51362 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,6 +161,10 @@ New Session With Profile No profile available No file picker found + Enter new URL + Enter new Repo + URL cannot be empty + Repo cannot be empty http://janyo.pw:82/kiva/neoterm diff --git a/app/src/test/java/io/neoterm/PackageManagerTest.kt b/app/src/test/java/io/neoterm/PackageManagerTest.kt index 836c7af..42d2822 100644 --- a/app/src/test/java/io/neoterm/PackageManagerTest.kt +++ b/app/src/test/java/io/neoterm/PackageManagerTest.kt @@ -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