diff --git a/build.gradle b/build.gradle index 3a8511b6..11f81db4 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { buildToolsVersion = '27.0.3' supportLibraryVersion = '27.1.1' takisoftFixVersion = '27.1.1.0' + roomVersion = '1.1.0' playServicesVersion = '12.0.1' junitVersion = '4.12' androidTestVersion = '1.0.2' diff --git a/mobile/build.gradle b/mobile/build.gradle index 42590c67..4c183dcd 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -4,6 +4,7 @@ import java.util.regex.Pattern apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' def getCurrentFlavor() { String task = getGradle().getStartParameter().getTaskRequests().toString() @@ -26,6 +27,11 @@ android { testApplicationId "com.github.shadowsocks.test" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW" + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } buildTypes { debug { @@ -54,6 +60,7 @@ android { dependencies { implementation project(':core') + implementation "android.arch.persistence.room:runtime:$roomVersion" implementation "com.android.support:customtabs:$supportLibraryVersion" implementation "com.android.support:design:$supportLibraryVersion" implementation "com.android.support:gridlayout-v7:$supportLibraryVersion" @@ -63,7 +70,6 @@ dependencies { implementation "com.google.android.gms:play-services-analytics:$playServicesVersion" implementation "com.google.android.gms:play-services-vision:$playServicesVersion" implementation "com.google.firebase:firebase-config:$playServicesVersion" - implementation 'com.j256.ormlite:ormlite-android:5.1' implementation 'com.mikepenz:materialdrawer:6.0.7' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion" @@ -73,6 +79,8 @@ dependencies { implementation 'net.glxn.qrgen:android:2.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3' + kapt "android.arch.persistence.room:compiler:$roomVersion" + testImplementation "android.arch.persistence.room:testing:$roomVersion" testImplementation "junit:junit:$junitVersion" androidTestImplementation "com.android.support.test:runner:$androidTestVersion" androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion" diff --git a/mobile/schemas/com.github.shadowsocks.database.PrivateDatabase/26.json b/mobile/schemas/com.github.shadowsocks.database.PrivateDatabase/26.json new file mode 100644 index 00000000..3f89b5d2 --- /dev/null +++ b/mobile/schemas/com.github.shadowsocks.database.PrivateDatabase/26.json @@ -0,0 +1,161 @@ +{ + "formatVersion": 1, + "database": { + "version": 26, + "identityHash": "a69accf94393d71abdb60873d4ea74a2", + "entities": [ + { + "tableName": "Profile", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "remotePort", + "columnName": "remotePort", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "method", + "columnName": "method", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "route", + "columnName": "route", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "remoteDns", + "columnName": "remoteDns", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proxyApps", + "columnName": "proxyApps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bypass", + "columnName": "bypass", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "udpdns", + "columnName": "udpdns", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ipv6", + "columnName": "ipv6", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "individual", + "columnName": "individual", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tx", + "columnName": "tx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rx", + "columnName": "rx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plugin", + "columnName": "plugin", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "KeyValuePair", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "valueType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a69accf94393d71abdb60873d4ea74a2\")" + ] + } +} \ No newline at end of file diff --git a/mobile/schemas/com.github.shadowsocks.database.PublicDatabase/3.json b/mobile/schemas/com.github.shadowsocks.database.PublicDatabase/3.json new file mode 100644 index 00000000..7861615f --- /dev/null +++ b/mobile/schemas/com.github.shadowsocks.database.PublicDatabase/3.json @@ -0,0 +1,45 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "f1aab1fb633378621635c344dbc8ac7b", + "entities": [ + { + "tableName": "KeyValuePair", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "valueType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"f1aab1fb633378621635c344dbc8ac7b\")" + ] + } +} \ No newline at end of file diff --git a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl index ce20ee7c..60ba3295 100644 --- a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl +++ b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl @@ -2,7 +2,7 @@ package com.github.shadowsocks.aidl; interface IShadowsocksServiceCallback { oneway void stateChanged(int state, String profileName, String msg); - oneway void trafficUpdated(int profileId, long txRate, long rxRate, long txTotal, long rxTotal); + oneway void trafficUpdated(long profileId, long txRate, long rxRate, long txTotal, long rxTotal); // Traffic data has persisted to database, listener should refetch their data from database - oneway void trafficPersisted(int profileId); + oneway void trafficPersisted(long profileId); } diff --git a/mobile/src/main/java/com/github/shadowsocks/App.kt b/mobile/src/main/java/com/github/shadowsocks/App.kt index a73a2452..32891ddf 100644 --- a/mobile/src/main/java/com/github/shadowsocks/App.kt +++ b/mobile/src/main/java/com/github/shadowsocks/App.kt @@ -57,7 +57,6 @@ import com.google.android.gms.analytics.StandardExceptionParser import com.google.android.gms.analytics.Tracker import com.google.firebase.FirebaseApp import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.j256.ormlite.logger.LocalLog import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat import java.io.File import java.io.IOException @@ -92,7 +91,7 @@ class App : Application() { val currentProfile: Profile? get() = if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId) - fun switchProfile(id: Int): Profile { + fun switchProfile(id: Long): Profile { val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile() DataStore.profileId = result.id return result @@ -116,7 +115,6 @@ class App : Application() { override fun onCreate() { super.onCreate() app = this - if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR") AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java, diff --git a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt index f3738a73..dae16f64 100644 --- a/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt +++ b/mobile/src/main/java/com/github/shadowsocks/MainActivity.kt @@ -116,10 +116,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe override fun stateChanged(state: Int, profileName: String?, msg: String?) { app.handler.post { changeState(state, msg, true) } } - override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { + override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { app.handler.post { updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal) } } - override fun trafficPersisted(profileId: Int) { + override fun trafficPersisted(profileId: Long) { app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) } } } @@ -148,7 +148,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state stateListener?.invoke(state) } - fun updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { + fun updateTraffic(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { txText.text = Formatter.formatFileSize(this, txTotal) rxText.text = Formatter.formatFileSize(this, rxTotal) txRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, txRate)) diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt index c7dcdd2e..85a2cbd9 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfileConfigFragment.kt @@ -56,7 +56,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 } - private var profileId = -1 + private var profileId = -1L private lateinit var isProxyApps: SwitchPreference private lateinit var plugin: IconListPreference private lateinit var pluginConfigure: EditTextPreference @@ -66,7 +66,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = DataStore.privateStore val activity = requireActivity() - profileId = activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1) + profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L) addPreferencesFromResource(R.xml.pref_profile) if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) { findPreference(Key.host).summary = "shadowsocks.example.org" diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt index 6b1ab9f4..ad0361af 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt @@ -68,7 +68,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { BaseService.CONNECTED, BaseService.STOPPED -> true else -> false } - private fun isProfileEditable(id: Int) = when ((activity as MainActivity).state) { + private fun isProfileEditable(id: Long) = when ((activity as MainActivity).state) { BaseService.CONNECTED -> id != DataStore.profileId BaseService.STOPPED -> true else -> false @@ -281,17 +281,17 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { for ((_, item) in actions) ProfileManager.delProfile(item.id) } - fun refreshId(id: Int) { + fun refreshId(id: Long) { val index = profiles.indexOfFirst { it.id == id } if (index >= 0) notifyItemChanged(index) } - fun deepRefreshId(id: Int) { + fun deepRefreshId(id: Long) { val index = profiles.indexOfFirst { it.id == id } if (index < 0) return profiles[index] = ProfileManager.getProfile(id)!! notifyItemChanged(index) } - fun removeId(id: Int) { + fun removeId(id: Long) { val index = profiles.indexOfFirst { it.id == id } if (index < 0) return profiles.removeAt(index) @@ -304,7 +304,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { val profilesAdapter by lazy { ProfilesAdapter() } private lateinit var undoManager: UndoSnackbarManager - private var bandwidthProfile: Int = 0 + private var bandwidthProfile = 0L private var txTotal: Long = 0L private var rxTotal: Long = 0L @@ -324,8 +324,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { toolbar.inflateMenu(R.menu.profile_manager_menu) toolbar.setOnMenuItemClickListener(this) - if (ProfileManager.getFirstProfile() == null) - DataStore.profileId = ProfileManager.createProfile().id + if (!ProfileManager.isNotEmpty()) DataStore.profileId = ProfileManager.createProfile().id val profilesList = view.findViewById(R.id.list) val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) profilesList.layoutManager = layoutManager @@ -378,7 +377,9 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { Snackbar.LENGTH_LONG).show() return true } - } catch (_: Exception) { } + } catch (exc: Exception) { + exc.printStackTrace() + } Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG).show() true @@ -399,8 +400,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { } } - override fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { - if (profileId != -1) { // ignore resets from MainActivity + override fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { + if (profileId != -1L) { // ignore resets from MainActivity if (bandwidthProfile != profileId) { onTrafficPersisted(bandwidthProfile) bandwidthProfile = profileId @@ -410,7 +411,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { profilesAdapter.refreshId(profileId) } } - fun onTrafficPersisted(profileId: Int) { + fun onTrafficPersisted(profileId: Long) { txTotal = 0 rxTotal = 0 if (bandwidthProfile != profileId) { diff --git a/mobile/src/main/java/com/github/shadowsocks/ToolbarFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ToolbarFragment.kt index bb49aa17..de2b7c92 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ToolbarFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ToolbarFragment.kt @@ -38,7 +38,7 @@ open class ToolbarFragment : Fragment() { activity.drawer.setToolbar(activity, toolbar, true) } - open fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { } + open fun onTrafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { } open fun onBackPressed(): Boolean = false } diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/ServiceNotification.kt b/mobile/src/main/java/com/github/shadowsocks/bg/ServiceNotification.kt index efcbc417..05641e23 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/ServiceNotification.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/ServiceNotification.kt @@ -54,7 +54,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam private val callback by lazy { object : IShadowsocksServiceCallback.Stub() { override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore - override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { + override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { service as Context val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, txRate)) val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, rxRate)) @@ -63,7 +63,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam Formatter.formatFileSize(service, txTotal), Formatter.formatFileSize(service, rxTotal))) show() } - override fun trafficPersisted(profileId: Int) { } + override fun trafficPersisted(profileId: Long) { } } } private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) } diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/TileService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/TileService.kt index f31394ea..3091dbcc 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/TileService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/TileService.kt @@ -64,8 +64,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface { tile.label = label ?: getString(R.string.app_name) tile.updateTile() } - override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { } - override fun trafficPersisted(profileId: Int) { } + override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { } + override fun trafficPersisted(profileId: Long) { } } } diff --git a/mobile/src/main/java/com/github/shadowsocks/database/KeyValuePair.kt b/mobile/src/main/java/com/github/shadowsocks/database/KeyValuePair.kt index 6998c15a..d36db0c6 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/KeyValuePair.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/KeyValuePair.kt @@ -20,37 +20,54 @@ package com.github.shadowsocks.database -import com.j256.ormlite.field.DataType -import com.j256.ormlite.field.DatabaseField +import android.arch.persistence.room.* import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +@Entity class KeyValuePair() { companion object { const val TYPE_UNINITIALIZED = 0 const val TYPE_BOOLEAN = 1 const val TYPE_FLOAT = 2 + @Deprecated("Use TYPE_LONG.") const val TYPE_INT = 3 const val TYPE_LONG = 4 const val TYPE_STRING = 5 const val TYPE_STRING_SET = 6 } - @DatabaseField(id = true) - var key: String? = "" - @DatabaseField + @android.arch.persistence.room.Dao + interface Dao { + @Query("SELECT * FROM `KeyValuePair` WHERE `key` = :key") + operator fun get(key: String): KeyValuePair? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun put(value: KeyValuePair): Long + + @Query("DELETE FROM `KeyValuePair` WHERE `key` = :key") + fun delete(key: String): Int + } + + @PrimaryKey + var key: String = "" var valueType: Int = TYPE_UNINITIALIZED - @DatabaseField(dataType = DataType.BYTE_ARRAY) var value: ByteArray = ByteArray(0) val boolean: Boolean? get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null val float: Float? get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null + @Suppress("DEPRECATION") + @Deprecated("Use long.", ReplaceWith("long")) val int: Int? get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null - val long: Long? - get() = if (valueType == TYPE_LONG) ByteBuffer.wrap(value).long else null + val long: Long? get() = when (valueType) { + @Suppress("DEPRECATION") + TYPE_INT -> ByteBuffer.wrap(value).int.toLong() + TYPE_LONG -> ByteBuffer.wrap(value).long + else -> null + } val string: String? get() = if (valueType == TYPE_STRING) String(value) else null val stringSet: Set? @@ -65,7 +82,8 @@ class KeyValuePair() { result } else null - constructor(key: String?) : this() { + @Ignore + constructor(key: String) : this() { this.key = key } @@ -80,6 +98,8 @@ class KeyValuePair() { this.value = ByteBuffer.allocate(4).putFloat(value).array() return this } + @Suppress("DEPRECATION") + @Deprecated("Use long.") fun put(value: Int): KeyValuePair { valueType = TYPE_INT this.value = ByteBuffer.allocate(4).putInt(value).array() diff --git a/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt index 52215aa4..86b3e306 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt @@ -20,137 +20,39 @@ package com.github.shadowsocks.database -import android.database.sqlite.SQLiteDatabase -import android.support.v7.preference.PreferenceManager -import android.text.TextUtils +import android.arch.persistence.db.SupportSQLiteDatabase +import android.arch.persistence.room.Database +import android.arch.persistence.room.Room +import android.arch.persistence.room.RoomDatabase import com.github.shadowsocks.App.Companion.app +import com.github.shadowsocks.database.migration.RecreateSchemaMigration import com.github.shadowsocks.utils.Key -import com.j256.ormlite.android.AndroidDatabaseConnection -import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper -import com.j256.ormlite.dao.Dao -import com.j256.ormlite.support.ConnectionSource -import com.j256.ormlite.table.TableUtils -object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) { - @Suppress("UNCHECKED_CAST") - val profileDao: Dao by lazy { getDao(Profile::class.java) as Dao } - @Suppress("UNCHECKED_CAST") - val kvPairDao: Dao by lazy { getDao(KeyValuePair::class.java) as Dao } - - override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) { - TableUtils.createTable(connectionSource, Profile::class.java) - TableUtils.createTable(connectionSource, KeyValuePair::class.java) - } - - private fun recreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) { - TableUtils.dropTable(connectionSource, Profile::class.java, true) - TableUtils.dropTable(connectionSource, KeyValuePair::class.java, true) - onCreate(database, connectionSource) - } - - override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?, - oldVersion: Int, newVersion: Int) { - if (oldVersion < 7) { - recreate(database, connectionSource) - return +@Database(entities = [Profile::class, KeyValuePair::class], version = 26) +abstract class PrivateDatabase : RoomDatabase() { + companion object { + private val instance by lazy { + Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE) + .addMigrations( + Migration26 + ) + .fallbackToDestructiveMigration() + .allowMainThreadQueries() + .build() } - try { - if (oldVersion < 8) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;") - } - if (oldVersion < 9) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';") - } else if (oldVersion < 19) { - profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;") - } - if (oldVersion < 11) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;") - } - if (oldVersion < 12) { - profileDao.executeRawNoArgs("BEGIN TRANSACTION;") - profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;") - TableUtils.createTable(connectionSource, Profile::class.java) - profileDao.executeRawNoArgs( - "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route," + - " proxyApps, bypass, udpdns, ipv6, individual) " + - "SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global," + - " bypass, udpdns, ipv6, individual FROM `tmp`;") - profileDao.executeRawNoArgs("DROP TABLE `tmp`;") - profileDao.executeRawNoArgs("COMMIT;") - } else if (oldVersion < 13) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;") - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;") - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;") - } - - if (oldVersion < 15) { - if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;") - var i = 0L - val apps by lazy { app.packageManager.getInstalledApplications(0) } - for (profile in profileDao.queryForAll()) { - if (oldVersion < 14) { - val uidSet = profile.individual.split('|').filter(TextUtils::isDigitsOnly) - .map(String::toInt).toSet() - profile.individual = apps.filter { uidSet.contains(it.uid) } - .joinToString("\n") { it.packageName } - } - profile.userOrder = i - profileDao.update(profile) - i += 1 - } - } - - if (oldVersion < 16) { - profileDao.executeRawNoArgs( - "UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'") - } - - if (oldVersion < 21) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';") - } - - if (oldVersion < 17) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;") - } else if (oldVersion < 22) { - // upgrade kcptun to SIP003 plugin - profileDao.executeRawNoArgs("BEGIN TRANSACTION;") - profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;") - TableUtils.createTable(connectionSource, Profile::class.java) - profileDao.executeRawNoArgs( - "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, " + - "remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " + - "plugin) " + - "SELECT id, name, host, localPort, " + - "CASE WHEN kcp = 1 THEN kcpPort ELSE remotePort END, password, method, route, " + - "remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " + - "CASE WHEN kcp = 1 THEN 'kcptun ' || kcpcli ELSE NULL END FROM `tmp`;") - profileDao.executeRawNoArgs("DROP TABLE `tmp`;") - profileDao.executeRawNoArgs("COMMIT;") - } - - if (oldVersion < 23) { - profileDao.executeRawNoArgs("BEGIN TRANSACTION;") - TableUtils.createTable(connectionSource, KeyValuePair::class.java) - profileDao.executeRawNoArgs("COMMIT;") - val old = PreferenceManager.getDefaultSharedPreferences(app) - PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0))) - PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false))) - } - - if (oldVersion < 25) { - PublicDatabase.onUpgrade(database, 0, -1) - } - } catch (ex: Exception) { - app.track(ex) - recreate(database, connectionSource) - } + val profileDao get() = instance.profileDao() + val kvPairDao get() = instance.keyValuePairDao() } + abstract fun profileDao(): Profile.Dao + abstract fun keyValuePairDao(): KeyValuePair.Dao - override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { - val connection = AndroidDatabaseConnection(db, true) - connectionSource.saveSpecialConnection(connection) - recreate(db, connectionSource) - connectionSource.clearSpecialConnection(connection) + private object Migration26 : RecreateSchemaMigration(25, 26, "Profile", + "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)", + "`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") { + override fun migrate(database: SupportSQLiteDatabase) { + super.migrate(database) + PublicDatabase.Migration3.migrate(database) + } } } diff --git a/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt b/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt index 2c9f480a..267fe5ca 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/Profile.kt @@ -20,6 +20,7 @@ package com.github.shadowsocks.database +import android.arch.persistence.room.* import android.net.Uri import android.util.Base64 import android.util.Log @@ -27,13 +28,12 @@ import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.parsePort -import com.j256.ormlite.field.DataType -import com.j256.ormlite.field.DatabaseField import java.io.Serializable import java.net.URI import java.net.URISyntaxException import java.util.* +@Entity class Profile : Serializable { companion object { private const val TAG = "ShadowParser" @@ -93,61 +93,50 @@ class Profile : Serializable { }.filterNotNull() } - @DatabaseField(generatedId = true) - var id: Int = 0 + @android.arch.persistence.room.Dao + interface Dao { + @Query("SELECT * FROM `Profile` WHERE `id` = :id") + operator fun get(id: Long): Profile? - @DatabaseField + @Query("SELECT * FROM `Profile` ORDER BY `userOrder`") + fun list(): List + + @Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`") + fun nextOrder(): Long? + + @Query("SELECT 1 FROM `Profile` LIMIT 1") + fun isNotEmpty(): Boolean + + @Insert + fun create(value: Profile): Long + + @Update + fun update(value: Profile): Int + + @Query("DELETE FROM `Profile` WHERE `id` = :id") + fun delete(id: Long): Int + } + + @PrimaryKey(autoGenerate = true) + var id: Long = 0 var name: String? = "" - - @DatabaseField var host: String = "198.199.101.152" - - @DatabaseField var remotePort: Int = 8388 - - @DatabaseField var password: String = "u1rRWTssNv0p" - - @DatabaseField var method: String = "aes-256-cfb" - - @DatabaseField var route: String = "all" - - @DatabaseField var remoteDns: String = "8.8.8.8" - - @DatabaseField var proxyApps: Boolean = false - - @DatabaseField var bypass: Boolean = false - - @DatabaseField var udpdns: Boolean = false - - @DatabaseField var ipv6: Boolean = true - - @DatabaseField(dataType = DataType.LONG_STRING) var individual: String = "" - - @DatabaseField var tx: Long = 0 - - @DatabaseField var rx: Long = 0 - - @DatabaseField - val date: Date = Date() - - @DatabaseField var userOrder: Long = 0 - - @DatabaseField var plugin: String? = null - // not persisted in db, only used by direct boot + @Ignore // not persisted in db, only used by direct boot var dirty: Boolean = false val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort) diff --git a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt index 904c1deb..413480cc 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt @@ -50,10 +50,8 @@ object ProfileManager { profile.individual = oldProfile.individual profile.udpdns = oldProfile.udpdns } - val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder() - .selectRaw("MAX(userOrder)").prepareStatementString()).firstResult - if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1 - PrivateDatabase.profileDao.createOrUpdate(profile) + profile.userOrder = PrivateDatabase.profileDao.nextOrder() ?: 0 + profile.id = PrivateDatabase.profileDao.create(profile) ProfilesFragment.instance?.profilesAdapter?.add(profile) return profile } @@ -62,11 +60,11 @@ object ProfileManager { * Note: It's caller's responsibility to update DirectBoot profile if necessary. */ @Throws(SQLException::class) - fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile) + fun updateProfile(profile: Profile) = check(PrivateDatabase.profileDao.update(profile) == 1) @Throws(IOException::class) - fun getProfile(id: Int): Profile? = try { - PrivateDatabase.profileDao.queryForId(id) + fun getProfile(id: Long): Profile? = try { + PrivateDatabase.profileDao[id] } catch (ex: SQLException) { if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex) Log.e(TAG, "getProfile", ex) @@ -75,25 +73,25 @@ object ProfileManager { } @Throws(SQLException::class) - fun delProfile(id: Int) { - PrivateDatabase.profileDao.deleteById(id) + fun delProfile(id: Long) { + check(PrivateDatabase.profileDao.delete(id) == 1) ProfilesFragment.instance?.profilesAdapter?.removeId(id) if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean() } @Throws(IOException::class) - fun getFirstProfile(): Profile? = try { - PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull() + fun isNotEmpty(): Boolean = try { + PrivateDatabase.profileDao.isNotEmpty() } catch (ex: SQLException) { if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex) - Log.e(TAG, "getFirstProfile", ex) + Log.e(TAG, "isEmpty", ex) app.track(ex) - null + true } @Throws(IOException::class) fun getAllProfiles(): List? = try { - PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare()) + PrivateDatabase.profileDao.list() } catch (ex: SQLException) { if (ex.cause is SQLiteCantOpenDatabaseException) throw IOException(ex) Log.e(TAG, "getAllProfiles", ex) diff --git a/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt b/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt index f1594a04..98e7d25e 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt @@ -20,42 +20,31 @@ package com.github.shadowsocks.database -import android.database.sqlite.SQLiteDatabase +import android.arch.persistence.room.Database +import android.arch.persistence.room.Room +import android.arch.persistence.room.RoomDatabase import com.github.shadowsocks.App.Companion.app -import com.github.shadowsocks.acl.Acl +import com.github.shadowsocks.database.migration.RecreateSchemaMigration import com.github.shadowsocks.utils.Key -import com.j256.ormlite.android.AndroidDatabaseConnection -import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper -import com.j256.ormlite.dao.Dao -import com.j256.ormlite.support.ConnectionSource -import com.j256.ormlite.table.TableUtils -object PublicDatabase : OrmLiteSqliteOpenHelper(app.deviceContext, Key.DB_PUBLIC, null, 2) { - @Suppress("UNCHECKED_CAST") - val kvPairDao: Dao by lazy { getDao(KeyValuePair::class.java) as Dao } - - override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) { - TableUtils.createTable(connectionSource, KeyValuePair::class.java) - } - - override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?, - oldVersion: Int, newVersion: Int) { - if (oldVersion < 1) { - PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key", - Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query() - .forEach { kvPairDao.createOrUpdate(it) } +@Database(entities = [(KeyValuePair::class)], version = 3) +abstract class PublicDatabase : RoomDatabase() { + companion object { + private val instance by lazy { + Room.databaseBuilder(app.deviceContext, PublicDatabase::class.java, Key.DB_PUBLIC) + .allowMainThreadQueries() + .addMigrations( + Migration3 + ) + .fallbackToDestructiveMigration() + .build() } - if (oldVersion < 2) { - kvPairDao.createOrUpdate(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString())) - } + val kvPairDao get() = instance.keyValuePairDao() } + abstract fun keyValuePairDao(): KeyValuePair.Dao - override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { - val connection = AndroidDatabaseConnection(db, true) - connectionSource.saveSpecialConnection(connection) - TableUtils.dropTable(connectionSource, KeyValuePair::class.java, true) - onCreate(db, connectionSource) - connectionSource.clearSpecialConnection(connection) - } + internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair", + "(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))", + "`key`, `valueType`, `value`") } diff --git a/mobile/src/main/java/com/github/shadowsocks/database/migration/RecreateSchemaMigration.kt b/mobile/src/main/java/com/github/shadowsocks/database/migration/RecreateSchemaMigration.kt new file mode 100644 index 00000000..3f50ab07 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/database/migration/RecreateSchemaMigration.kt @@ -0,0 +1,35 @@ +/******************************************************************************* + * * + * Copyright (C) 2018 by Max Lv * + * Copyright (C) 2018 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.database.migration + +import android.arch.persistence.db.SupportSQLiteDatabase +import android.arch.persistence.room.migration.Migration + +open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String, + private val schema: String, private val keys: String) : + Migration(oldVersion, newVersion) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE `tmp` $schema") + database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`") + database.execSQL("DROP TABLE `$table`") + database.execSQL("ALTER TABLE `tmp` RENAME TO `$table`") + } +} diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt index 144f2eab..ecc1d2c2 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/DataStore.kt @@ -29,9 +29,9 @@ import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.parsePort object DataStore { - val publicStore = OrmLitePreferenceDataStore(PublicDatabase.kvPairDao) + val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao) // privateStore will only be used as temp storage for ProfileConfigFragment - val privateStore = OrmLitePreferenceDataStore(PrivateDatabase.kvPairDao) + val privateStore = RoomPreferenceDataStore(PrivateDatabase.kvPairDao) // hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() } @@ -43,10 +43,10 @@ object DataStore { } else parsePort(publicStore.getString(key), default + userIndex) } - var profileId: Int - get() = publicStore.getInt(Key.id) ?: 0 + var profileId: Long + get() = publicStore.getLong(Key.id) ?: 0 set(value) { - publicStore.putInt(Key.id, value) + publicStore.putLong(Key.id, value) if (DataStore.directBootAware) DirectBoot.update() } val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt deleted file mode 100644 index 19849133..00000000 --- a/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt +++ /dev/null @@ -1,82 +0,0 @@ -/******************************************************************************* - * * - * Copyright (C) 2017 by Max Lv * - * Copyright (C) 2017 by Mygod Studio * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - * * - *******************************************************************************/ - -package com.github.shadowsocks.preference - -import android.support.v7.preference.PreferenceDataStore -import com.github.shadowsocks.database.KeyValuePair -import com.j256.ormlite.dao.Dao -import java.util.HashSet - -@Suppress("MemberVisibilityCanPrivate", "unused") -open class OrmLitePreferenceDataStore(private val kvPairDao: Dao) : PreferenceDataStore() { - fun getBoolean(key: String?) = kvPairDao.queryForId(key)?.boolean - fun getFloat(key: String?) = kvPairDao.queryForId(key)?.float - fun getInt(key: String?) = kvPairDao.queryForId(key)?.int - fun getLong(key: String?) = kvPairDao.queryForId(key)?.long - fun getString(key: String?) = kvPairDao.queryForId(key)?.string - fun getStringSet(key: String?) = kvPairDao.queryForId(key)?.stringSet - - override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue - override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue - override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue - override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue - override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue - override fun getStringSet(key: String?, defValue: MutableSet?) = getStringSet(key) ?: defValue - - fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value) - fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value) - fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value) - fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value) - override fun putBoolean(key: String?, value: Boolean) { - kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) - fireChangeListener(key) - } - override fun putFloat(key: String?, value: Float) { - kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) - fireChangeListener(key) - } - override fun putInt(key: String?, value: Int) { - kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) - fireChangeListener(key) - } - override fun putLong(key: String?, value: Long) { - kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) - fireChangeListener(key) - } - override fun putString(key: String?, value: String?) = if (value == null) remove(key) else { - kvPairDao.createOrUpdate(KeyValuePair(key).put(value)) - fireChangeListener(key) - } - override fun putStringSet(key: String?, values: MutableSet?) = if (values == null) remove(key) else { - kvPairDao.createOrUpdate(KeyValuePair(key).put(values)) - fireChangeListener(key) - } - - fun remove(key: String?) { - kvPairDao.deleteById(key) - fireChangeListener(key) - } - - private val listeners = HashSet() - private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) } - fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener) - fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener) -} diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/RoomPreferenceDataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/RoomPreferenceDataStore.kt new file mode 100644 index 00000000..dd5a39f3 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/preference/RoomPreferenceDataStore.kt @@ -0,0 +1,81 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package com.github.shadowsocks.preference + +import android.support.v7.preference.PreferenceDataStore +import com.github.shadowsocks.database.KeyValuePair +import java.util.HashSet + +@Suppress("MemberVisibilityCanBePrivate", "unused") +open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() { + fun getBoolean(key: String) = kvPairDao[key]?.boolean + fun getFloat(key: String) = kvPairDao[key]?.float + fun getInt(key: String) = kvPairDao[key]?.long?.toInt() + fun getLong(key: String) = kvPairDao[key]?.long + fun getString(key: String) = kvPairDao[key]?.string + fun getStringSet(key: String) = kvPairDao[key]?.stringSet + + override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue + override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue + override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue + override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue + override fun getString(key: String, defValue: String?) = getString(key) ?: defValue + override fun getStringSet(key: String, defValue: MutableSet?) = getStringSet(key) ?: defValue + + fun putBoolean(key: String, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value) + fun putFloat(key: String, value: Float?) = if (value == null) remove(key) else putFloat(key, value) + fun putInt(key: String, value: Int?) = if (value == null) remove(key) else putLong(key, value.toLong()) + fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value) + override fun putBoolean(key: String, value: Boolean) { + kvPairDao.put(KeyValuePair(key).put(value)) + fireChangeListener(key) + } + override fun putFloat(key: String, value: Float) { + kvPairDao.put(KeyValuePair(key).put(value)) + fireChangeListener(key) + } + override fun putInt(key: String, value: Int) { + kvPairDao.put(KeyValuePair(key).put(value.toLong())) + fireChangeListener(key) + } + override fun putLong(key: String, value: Long) { + kvPairDao.put(KeyValuePair(key).put(value)) + fireChangeListener(key) + } + override fun putString(key: String, value: String?) = if (value == null) remove(key) else { + kvPairDao.put(KeyValuePair(key).put(value)) + fireChangeListener(key) + } + override fun putStringSet(key: String, values: MutableSet?) = if (values == null) remove(key) else { + kvPairDao.put(KeyValuePair(key).put(values)) + fireChangeListener(key) + } + + fun remove(key: String) { + kvPairDao.delete(key) + fireChangeListener(key) + } + + private val listeners = HashSet() + private fun fireChangeListener(key: String) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) } + fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener) + fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener) +} diff --git a/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt b/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt index 79af911d..6c45ab1c 100644 --- a/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt +++ b/mobile/src/main/java/com/github/shadowsocks/tasker/Settings.kt @@ -36,12 +36,17 @@ class Settings(bundle: Bundle?) { } var switchOn: Boolean = bundle?.getBoolean(KEY_SWITCH_ON, true) ?: true - var profileId: Int = bundle?.getInt(KEY_PROFILE_ID, -1) ?: -1 + var profileId: Long + + init { + profileId = bundle?.getLong(KEY_PROFILE_ID, -1L) ?: -1L + if (profileId < 0) profileId = (bundle?.getInt(KEY_PROFILE_ID, -1) ?: -1).toLong() + } fun toIntent(context: Context): Intent { val bundle = Bundle() if (!switchOn) bundle.putBoolean(KEY_SWITCH_ON, false) - if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId) + if (profileId >= 0) bundle.putLong(KEY_PROFILE_ID, profileId) val profile = ProfileManager.getProfile(profileId) return Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB, if (profile != null) context.getString(if (switchOn) R.string.start_service else R.string.stop_service, diff --git a/mobile/src/test/java/com/github/shadowsocks/database/KeyValuePairTest.kt b/mobile/src/test/java/com/github/shadowsocks/database/KeyValuePairTest.kt index e5f0d8ca..66bb9646 100644 --- a/mobile/src/test/java/com/github/shadowsocks/database/KeyValuePairTest.kt +++ b/mobile/src/test/java/com/github/shadowsocks/database/KeyValuePairTest.kt @@ -29,7 +29,8 @@ class KeyValuePairTest { val kvp = KeyValuePair() Assert.assertEquals(true, kvp.put(true).boolean) Assert.assertEquals(3f, kvp.put(3f).float) - Assert.assertEquals(3, kvp.put(3).int) + @Suppress("DEPRECATION") + Assert.assertEquals(3L, kvp.put(3).long) Assert.assertEquals(3L, kvp.put(3L).long) Assert.assertEquals("3", kvp.put("3").string) val set = (0 until 3).map(Int::toString).toSet()