From 3c532f1ac527beecabe77ec94374a025ecd563e9 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 23 Mar 2018 01:55:04 -0700 Subject: [PATCH] Fix SQLiteDatabaseLockedException --- .../github/shadowsocks/database/LockUtils.kt | 61 ++++++++++++++++ .../shadowsocks/database/PrivateDatabase.kt | 70 +++++++++---------- .../shadowsocks/database/ProfileManager.kt | 22 ++++-- .../shadowsocks/database/PublicDatabase.kt | 11 +-- .../preference/OrmLitePreferenceDataStore.kt | 29 ++++---- 5 files changed, 133 insertions(+), 60 deletions(-) create mode 100644 mobile/src/main/java/com/github/shadowsocks/database/LockUtils.kt diff --git a/mobile/src/main/java/com/github/shadowsocks/database/LockUtils.kt b/mobile/src/main/java/com/github/shadowsocks/database/LockUtils.kt new file mode 100644 index 00000000..d04a9a18 --- /dev/null +++ b/mobile/src/main/java/com/github/shadowsocks/database/LockUtils.kt @@ -0,0 +1,61 @@ +/******************************************************************************* + * * + * 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.database + +import android.database.sqlite.SQLiteDatabaseLockedException +import com.j256.ormlite.dao.Dao +import com.j256.ormlite.support.ConnectionSource +import com.j256.ormlite.table.TableUtils +import java.sql.SQLException + +private val Throwable.ultimateCause: Throwable get() { + var result = this + while (true) { + val cause = result.cause ?: return result + result = cause + } +} + +@Throws(SQLException::class) +fun safeWrapper(func: () -> T): T { + while (true) { + try { + return func() + } catch (e: SQLException) { + if (e.ultimateCause !is SQLiteDatabaseLockedException) throw e + } + } +} + +@Throws(SQLException::class) +inline fun ConnectionSource.createTableSafe() = safeWrapper { TableUtils.createTable(this, T::class.java) } +@Throws(SQLException::class) +fun Dao.queryAllSafe(): MutableList = safeWrapper { queryForAll() } +@Throws(SQLException::class) +fun Dao.queryByIdSafe(id: ID?): T? = safeWrapper { queryForId(id) } +@Throws(SQLException::class) +fun Dao.updateSafe(data: T?): Int = safeWrapper { update(data) } +@Throws(SQLException::class) +fun Dao.replaceSafe(data: T?): Dao.CreateOrUpdateStatus? = safeWrapper { createOrUpdate(data) } +@Throws(SQLException::class) +fun Dao.deleteByIdSafe(id: ID?) = safeWrapper { deleteById(id) } +@Throws(SQLException::class) +fun Dao.executeSafe(statement: String?) = safeWrapper { executeRawNoArgs(statement) } 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..f77e4862 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/PrivateDatabase.kt @@ -37,18 +37,18 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) @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) + override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource) { + connectionSource.createTableSafe() + connectionSource.createTableSafe() } - private fun recreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) { + 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?, + override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource, oldVersion: Int, newVersion: Int) { if (oldVersion < 7) { recreate(database, connectionSource) @@ -57,38 +57,38 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) try { if (oldVersion < 8) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;") + profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;") } if (oldVersion < 9) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';") + profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';") } else if (oldVersion < 19) { - profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;") + profileDao.executeSafe("UPDATE `profile` SET route = 'all' WHERE route IS NULL;") } if (oldVersion < 11) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;") + profileDao.executeSafe("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( + profileDao.executeSafe("BEGIN TRANSACTION;") + profileDao.executeSafe("ALTER TABLE `profile` RENAME TO `tmp`;") + connectionSource.createTableSafe() + profileDao.executeSafe( "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;") + profileDao.executeSafe("DROP TABLE `tmp`;") + profileDao.executeSafe("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;") + profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN tx LONG;") + profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN rx LONG;") + profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN date VARCHAR;") } if (oldVersion < 15) { - if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;") + if (oldVersion >= 12) profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN userOrder LONG;") var i = 0L val apps by lazy { app.packageManager.getInstalledApplications(0) } - for (profile in profileDao.queryForAll()) { + for (profile in profileDao.queryAllSafe()) { if (oldVersion < 14) { val uidSet = profile.individual.split('|').filter(TextUtils::isDigitsOnly) .map(String::toInt).toSet() @@ -96,28 +96,28 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) .joinToString("\n") { it.packageName } } profile.userOrder = i - profileDao.update(profile) + profileDao.updateSafe(profile) i += 1 } } if (oldVersion < 16) { - profileDao.executeRawNoArgs( + profileDao.executeSafe( "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';") + profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';") } if (oldVersion < 17) { - profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;") + profileDao.executeSafe("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( + profileDao.executeSafe("BEGIN TRANSACTION;") + profileDao.executeSafe("ALTER TABLE `profile` RENAME TO `tmp`;") + connectionSource.createTableSafe() + profileDao.executeSafe( "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, " + "remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " + "plugin) " + @@ -125,17 +125,17 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) "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;") + profileDao.executeSafe("DROP TABLE `tmp`;") + profileDao.executeSafe("COMMIT;") } if (oldVersion < 23) { - profileDao.executeRawNoArgs("BEGIN TRANSACTION;") - TableUtils.createTable(connectionSource, KeyValuePair::class.java) - profileDao.executeRawNoArgs("COMMIT;") + profileDao.executeSafe("BEGIN TRANSACTION;") + connectionSource.createTableSafe() + profileDao.executeSafe("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))) + PublicDatabase.kvPairDao.replaceSafe(KeyValuePair(Key.id).put(old.getInt(Key.id, 0))) + PublicDatabase.kvPairDao.replaceSafe(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false))) } if (oldVersion < 25) { 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 15ef7c82..2f81a725 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/ProfileManager.kt @@ -48,10 +48,12 @@ object ProfileManager { profile.individual = oldProfile.individual profile.udpdns = oldProfile.udpdns } - val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder() + val last = safeWrapper { + 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) + PrivateDatabase.profileDao.replaceSafe(profile) ProfilesFragment.instance?.profilesAdapter?.add(profile) return profile } @@ -60,10 +62,10 @@ 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) = PrivateDatabase.profileDao.updateSafe(profile) fun getProfile(id: Int): Profile? = try { - PrivateDatabase.profileDao.queryForId(id) + PrivateDatabase.profileDao.queryByIdSafe(id) } catch (ex: SQLException) { Log.e(TAG, "getProfile", ex) app.track(ex) @@ -72,13 +74,16 @@ object ProfileManager { @Throws(SQLException::class) fun delProfile(id: Int) { - PrivateDatabase.profileDao.deleteById(id) + PrivateDatabase.profileDao.deleteByIdSafe(id) ProfilesFragment.instance?.profilesAdapter?.removeId(id) if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean() } fun getFirstProfile(): Profile? = try { - PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull() + safeWrapper { + PrivateDatabase.profileDao.query( + PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull() + } } catch (ex: SQLException) { Log.e(TAG, "getFirstProfile", ex) app.track(ex) @@ -86,7 +91,10 @@ object ProfileManager { } fun getAllProfiles(): List? = try { - PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare()) + safeWrapper { + PrivateDatabase.profileDao.query( + PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare()) + } } catch (ex: SQLException) { Log.e(TAG, "getAllProfiles", ex) app.track(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..c7cfb394 100644 --- a/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt +++ b/mobile/src/main/java/com/github/shadowsocks/database/PublicDatabase.kt @@ -34,20 +34,21 @@ object PublicDatabase : OrmLiteSqliteOpenHelper(app.deviceContext, Key.DB_PUBLIC @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 onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource) { + connectionSource.createTableSafe() } override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?, oldVersion: Int, newVersion: Int) { if (oldVersion < 1) { - PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key", + safeWrapper { + PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key", Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query() - .forEach { kvPairDao.createOrUpdate(it) } + }.forEach { kvPairDao.replaceSafe(it) } } if (oldVersion < 2) { - kvPairDao.createOrUpdate(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString())) + kvPairDao.replaceSafe(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString())) } } diff --git a/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt b/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt index 19849133..b43d42bd 100644 --- a/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt +++ b/mobile/src/main/java/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.kt @@ -22,17 +22,20 @@ package com.github.shadowsocks.preference import android.support.v7.preference.PreferenceDataStore import com.github.shadowsocks.database.KeyValuePair +import com.github.shadowsocks.database.deleteByIdSafe +import com.github.shadowsocks.database.queryByIdSafe +import com.github.shadowsocks.database.replaceSafe 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 + fun getBoolean(key: String?) = kvPairDao.queryByIdSafe(key)?.boolean + fun getFloat(key: String?) = kvPairDao.queryByIdSafe(key)?.float + fun getInt(key: String?) = kvPairDao.queryByIdSafe(key)?.int + fun getLong(key: String?) = kvPairDao.queryByIdSafe(key)?.long + fun getString(key: String?) = kvPairDao.queryByIdSafe(key)?.string + fun getStringSet(key: String?) = kvPairDao.queryByIdSafe(key)?.stringSet override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue @@ -46,32 +49,32 @@ open class OrmLitePreferenceDataStore(private val kvPairDao: Dao?) = if (values == null) remove(key) else { - kvPairDao.createOrUpdate(KeyValuePair(key).put(values)) + kvPairDao.replaceSafe(KeyValuePair(key).put(values)) fireChangeListener(key) } fun remove(key: String?) { - kvPairDao.deleteById(key) + kvPairDao.deleteByIdSafe(key) fireChangeListener(key) }