Commit b66beecc authored by Javier Romero's avatar Javier Romero

Merge branch '10-ssh-support' into 'master'

Resolve "SSH support"

Closes #10

See merge request !14
parents ada71b95 19f89fd7
Pipeline #30162165 passed with stages
in 15 minutes and 47 seconds
## [Unreleased]
### Added
- SSH tunnel support
### Changed
- Redesign of connection editing form
## [4.0.10] - 2018-09-08
### Changed
......
# Connect2SQL
[![Codacy Badge][codacy-badge-image]][codacy-badge-link] [![Play Store][play-badge-image]][play-badge-link] [![Patreon][patreon-badge-image]][patreon-badge-link]
# Connect2SQL
Connect2SQL is a sql client that currently supports MySQL, MsSQL, PostgreSQL, and Sybase for Android.
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="200" />][play-store]
## Contribute
Contributing is as easy as...
1. Looking at the list of [issues "Accepting MRs"][accepting-mr-issues].
2. Assigning it to yourself.
3. Submitting a Merge Request.
[play-store]: https://play.google.com/store/apps/details?id=app.devlife.connect2sql
[accepting-mr-issues]: https://gitlab.com/devlife-apps/connect2sql/issues?scope=all&utf8=%E2%9C%93&state=opened&label_name[]=Accepting%20MRs
\ No newline at end of file
[accepting-mr-issues]: https://gitlab.com/devlife-apps/connect2sql/issues?scope=all&utf8=%E2%9C%93&state=opened&label_name[]=Accepting%20MRs
[codacy-badge-image]: https://api.codacy.com/project/badge/Grade/2872bdddc1dd4a15a6b3e7ad89fb4681
[codacy-badge-link]: https://www.codacy.com/app/devlife-apps/connect2sql
[patreon-badge-image]: https://img.shields.io/badge/Patreon-donate-orange.svg
[patreon-badge-link]: https://www.patreon.com/devlife
[play-badge-image]: https://img.shields.io/badge/Play%20Store-download-green.svg
[play-badge-link]: https://play.google.com/store/apps/details?id=app.devlife.connect2sql
\ No newline at end of file
......@@ -111,9 +111,11 @@ android {
ext {
version_answers = '1.4.3'
version_constraint_layout = '1.1.3'
version_crashlytcs = '2.9.5'
version_dagger = '2.16'
version_jsr250 = '1.0'
version_jsch = '0.1.54'
version_logback_android = '1.1.1-12'
version_mariadb = '1.7.4'
version_dexmaker_mockito = '2.19.0'
......@@ -127,11 +129,11 @@ ext {
dependencies {
implementation fileTree(include: '*.jar', dir: 'libs')
implementation "com.mobsandgeeks:android-saripaar:${version_saripaar}"
implementation "com.android.support:support-v4:${version_support}"
implementation "com.android.support:support-annotations:${version_support}"
implementation "com.android.support:appcompat-v7:${version_support}"
implementation "com.android.support:cardview-v7:${version_support}"
implementation "com.android.support.constraint:constraint-layout:${version_constraint_layout}"
implementation "com.android.support:customtabs:${version_support}"
implementation "com.android.support:design:${version_support}"
implementation "com.android.support:recyclerview-v7:${version_support}"
......@@ -147,14 +149,15 @@ dependencies {
implementation "com.google.dagger:dagger:${version_dagger}"
implementation "com.google.dagger:dagger-android:${version_dagger}"
implementation "com.google.dagger:dagger-android-support:${version_dagger}"
implementation "com.jcraft:jsch:${version_jsch}"
implementation "com.linkedin.dexmaker:dexmaker-mockito:${version_dexmaker_mockito}"
implementation "com.mobsandgeeks:android-saripaar:${version_saripaar}"
implementation "javax.annotation:jsr250-api:${version_jsr250}"
implementation "io.reactivex:rxandroid:${version_rx}"
implementation "io.reactivex:rxjava:${version_rx}"
implementation "me.zhanghai.android.patternlock:library:${version_patternlock}"
implementation "net.zetetic:android-database-sqlcipher:${version_sqlcipher}"
implementation "org.mariadb.jdbc:mariadb-java-client:${version_mariadb}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${version_kotlin}"
implementation "org.mariadb.jdbc:mariadb-java-client:${version_mariadb}"
implementation "org.slf4j:slf4j-api:${version_slf4j}"
}
......@@ -429,7 +429,7 @@ style:
NestedClassesVisibility:
active: false
NewLineAtEndOfFile:
active: true
active: false
NoTabs:
active: false
OptionalAbstractKeyword:
......
......@@ -43,6 +43,9 @@
android:name="app.devlife.connect2sql.ui.connection.ConnectionInfoEditorActivity"
android:label="@string/activity_configure_connection"
android:theme="@style/Theme.C2SQL.NoActionBar" />
<activity
android:name="app.devlife.connect2sql.ui.hostkeys.HostKeysActivity"
android:label="@string/activity_host_keys" />
<activity
android:name="app.devlife.connect2sql.ui.query.QueryActivity"
android:configChanges="orientation|uiMode|screenSize|smallestScreenSize|keyboardHidden"
......@@ -68,6 +71,7 @@
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabricApiKey}" />
</application>
</manifest>
</manifest>
\ No newline at end of file
......@@ -3,9 +3,6 @@ package app.devlife.connect2sql;
import android.content.Context;
import android.content.Intent;
/**
*
*/
public class ApplicationUtils {
public static Connect2SqlApplication getApplication(Context context) {
return ((Connect2SqlApplication) context.getApplicationContext());
......
......@@ -3,21 +3,22 @@ package app.devlife.connect2sql;
import android.app.Activity;
import android.app.Application;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
import app.devlife.connect2sql.activity.LaunchActivity;
import app.devlife.connect2sql.data.LockManager;
import app.devlife.connect2sql.di.AnalyticsModule;
import app.devlife.connect2sql.di.ApplicationComponent;
import app.devlife.connect2sql.di.ApplicationModule;
import app.devlife.connect2sql.di.ConnectionModule;
import app.devlife.connect2sql.di.DaggerApplicationComponent;
import app.devlife.connect2sql.di.DatabaseModule;
import app.devlife.connect2sql.di.PreferencesModule;
import app.devlife.connect2sql.di.SecurityModule;
import io.fabric.sdk.android.Fabric;
import app.devlife.connect2sql.activity.LaunchActivity;
import app.devlife.connect2sql.data.LockManager;
import app.devlife.connect2sql.di.*;
import app.devlife.connect2sql.log.EzLogger;
import javax.inject.Inject;
import java.lang.ref.WeakReference;
import io.fabric.sdk.android.Fabric;
/**
*
......@@ -26,21 +27,25 @@ public class Connect2SqlApplication extends Application {
private ApplicationComponent mApplicationComponent;
@Inject ApplicationFocusManager mApplicationFocusManager;
@Inject LockManager mLockManager;
@Inject Fabric mFabric;
@Inject
ApplicationFocusManager mApplicationFocusManager;
@Inject
LockManager mLockManager;
@Inject
Fabric mFabric;
@Override
public void onCreate() {
super.onCreate();
mApplicationComponent = DaggerApplicationComponent.builder()
.analyticsModule(new AnalyticsModule(this))
.applicationModule(new ApplicationModule(this))
.databaseModule(new DatabaseModule(this))
.preferencesModule(new PreferencesModule(this))
.securityModule(new SecurityModule(this))
.build();
.analyticsModule(new AnalyticsModule(this))
.applicationModule(new ApplicationModule(this))
.connectionModule(new ConnectionModule(this))
.databaseModule(new DatabaseModule(this))
.preferencesModule(new PreferencesModule(this))
.securityModule(new SecurityModule(this))
.build();
mApplicationComponent.inject(this);
mApplicationFocusManager.addOnFocusChangeListener(mOnFocusChangeListener);
......@@ -63,8 +68,8 @@ public class Connect2SqlApplication extends Application {
EzLogger.d("Last focused activity: " + activity);
if (!activity.getClass().equals(LaunchActivity.class)) {
if (!mLockManager.isSetLockActivity(activity) &&
!mLockManager.isUnlockActivity(activity) &&
!mLockManager.isForgotLockActivity(activity)) {
!mLockManager.isUnlockActivity(activity) &&
!mLockManager.isForgotLockActivity(activity)) {
mLockManager.startUnlockActivity(activity, 0);
} else {
EzLogger.d("Activity is a lock specific activity.");
......
......@@ -19,13 +19,9 @@ import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import com.crashlytics.android.answers.Answers
import com.crashlytics.android.answers.CustomEvent
import com.gitlab.connect2sql.R
import kotlinx.android.synthetic.main.activity_connections.connections_dashboard
import kotlinx.android.synthetic.main.activity_connections.fab
import app.devlife.connect2sql.ApplicationUtils
import app.devlife.connect2sql.connection.ConnectionAgent
import app.devlife.connect2sql.connection.SshTunnelAgent
import app.devlife.connect2sql.db.model.connection.ConnectionInfo
import app.devlife.connect2sql.db.model.connection.ConnectionInfoSqlModel
import app.devlife.connect2sql.db.repo.ConnectionInfoRepository
......@@ -34,10 +30,17 @@ import app.devlife.connect2sql.sql.DriverType
import app.devlife.connect2sql.ui.connection.ConnectionInfoDriverChooserActivity
import app.devlife.connect2sql.ui.connection.ConnectionInfoEditorActivity
import app.devlife.connect2sql.ui.connection.ConnectionInfoEditorRequest
import app.devlife.connect2sql.ui.hostkeys.HostKeysActivity
import app.devlife.connect2sql.ui.query.QueryActivity
import app.devlife.connect2sql.ui.widget.BlockItem
import app.devlife.connect2sql.ui.widget.Toast
import app.devlife.connect2sql.ui.widget.dialog.ProgressDialog
import com.crashlytics.android.answers.Answers
import com.crashlytics.android.answers.CustomEvent
import com.gitlab.connect2sql.R
import com.jcraft.jsch.JSch
import kotlinx.android.synthetic.main.activity_connections.connections_dashboard
import kotlinx.android.synthetic.main.activity_connections.fab
import rx.Subscriber
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
......@@ -49,6 +52,8 @@ class DashboardActivity : BaseActivity() {
private var actionMode: ActionMode? = null
@Inject
lateinit var jSch: JSch
@Inject
lateinit var connectionAgent: ConnectionAgent
@Inject
......@@ -72,7 +77,9 @@ class DashboardActivity : BaseActivity() {
override fun onResume() {
super.onResume()
supportLoaderManager.restartLoader(LOADER_CONNECTIONS, Bundle(), mConnectionsLoaderCallbacks)
supportLoaderManager.restartLoader(LOADER_CONNECTIONS,
Bundle(),
mConnectionsLoaderCallbacks)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
......@@ -82,9 +89,12 @@ class DashboardActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.host_keys ->
startActivity(HostKeysActivity.newIntent(this))
R.id.rate -> {
try {
val goToMarket = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName"))
val goToMarket = Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=$packageName"))
goToMarket.addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY
or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
......@@ -95,7 +105,8 @@ class DashboardActivity : BaseActivity() {
CustomTabsIntent.Builder()
.setToolbarColor(resources.getColor(R.color.blueBase, theme))
.build()
.launchUrl(this, Uri.parse("http://play.google.com/store/apps/details?id=$packageName"))
.launchUrl(this,
Uri.parse("http://play.google.com/store/apps/details?id=$packageName"))
}
}
R.id.beta -> {
......@@ -115,7 +126,8 @@ class DashboardActivity : BaseActivity() {
}
private fun connect(connectionInfo: ConnectionInfo) {
mAnswers.logCustom(CustomEvent("connect").putCustomAttribute("DriverType", connectionInfo.driverType.toString()))
mAnswers.logCustom(CustomEvent("connect").putCustomAttribute("DriverType",
connectionInfo.driverType.toString()))
// define a progress dialog to display
val progressDialog = ProgressDialog(
......@@ -134,17 +146,36 @@ class DashboardActivity : BaseActivity() {
override fun onError(e: Throwable?) {
progressDialog.dismiss()
val builder = AlertDialog.Builder(this@DashboardActivity)
builder.setTitle("Error")
builder.setMessage("Application Error: ${e?.message}")
builder.setNeutralButton("OK", null)
builder.create().show()
when (e) {
is SshTunnelAgent.UnknownHostException -> AlertDialog.Builder(this@DashboardActivity)
.setTitle(R.string.dialog_add_host_key)
.setMessage(getString(
R.string.dialog_host_fingerprint,
e.hostKey.host,
e.hostKey.getFingerPrint(jSch)
))
.setNegativeButton(R.string.dialog_no, null)
.setPositiveButton(R.string.dialog_yes) { dialog, _ ->
dialog.dismiss()
jSch.hostKeyRepository.add(e.hostKey, null)
connect(connectionInfo)
}
.create()
.show()
else -> AlertDialog.Builder(this@DashboardActivity)
.setTitle(R.string.dialog_error)
.setMessage("Couldn't connect:\n\n${e?.message}")
.setNeutralButton(R.string.dialog_ok, null)
.create()
.show()
}
}
override fun onNext(t: Connection?) {
progressDialog.dismiss()
startActivity(QueryActivity.newIntent(this@DashboardActivity, connectionInfo.id))
startActivity(QueryActivity.newIntent(this@DashboardActivity,
connectionInfo.id))
}
})
......@@ -187,7 +218,7 @@ class DashboardActivity : BaseActivity() {
val activatedItems = ArrayList<BlockItem>()
val totalChildren = connections_dashboard.childCount
for (i in 0..totalChildren - 1) {
for (i in 0 until totalChildren) {
val item = connections_dashboard.getChildAt(i) as BlockItem
if (item.isActivated) {
activatedItems.add(item)
......@@ -247,7 +278,8 @@ class DashboardActivity : BaseActivity() {
val connectionInfo = (item.tag as ConnectionInfo)
if (TextUtils.isEmpty(connectionInfo.password)) {
val promptDialogView = LayoutInflater.from(this@DashboardActivity).inflate(R.layout.dialog_prompt, null)
val promptDialogView = LayoutInflater.from(this@DashboardActivity)
.inflate(R.layout.dialog_prompt, null)
(promptDialogView.findViewById(R.id.textView1) as TextView).visibility = View.GONE
......@@ -302,7 +334,6 @@ class DashboardActivity : BaseActivity() {
private val mActionModeCallback = object : ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
// TODO Auto-generated method stub
return false
}
......@@ -334,7 +365,8 @@ class DashboardActivity : BaseActivity() {
for (connectionItem in activatedBlockItems) {
val connectionInfo = connectionItem.tag as ConnectionInfo
val request = ConnectionInfoEditorRequest(connectionInfo.id)
val newIntent = ConnectionInfoEditorActivity.newIntent(this@DashboardActivity, request)
val newIntent = ConnectionInfoEditorActivity.newIntent(this@DashboardActivity,
request)
startActivity(newIntent)
break
}
......@@ -345,9 +377,12 @@ class DashboardActivity : BaseActivity() {
for (connectionItem in activatedBlockItems) {
try {
val connectionInfo = connectionItem.tag as ConnectionInfo
connectionInfoRepository.save(connectionInfo.copy(id = -1, name = "Copy: " + connectionInfo.name))
connectionInfoRepository.save(connectionInfo.copy(id = -1,
name = "Copy: " + connectionInfo.name))
} catch (e: CloneNotSupportedException) {
Toast.makeText(this@DashboardActivity, "Failed to create copy", Toast.LENGTH_SHORT).show()
Toast.makeText(this@DashboardActivity,
"Failed to create copy",
Toast.LENGTH_SHORT).show()
e.printStackTrace()
}
}
......
package app.devlife.connect2sql.connection
import app.devlife.connect2sql.db.model.connection.Address
import app.devlife.connect2sql.db.model.connection.ConnectionInfo
import app.devlife.connect2sql.db.model.connection.SshTunnelConfig
import app.devlife.connect2sql.log.EzLogger
import app.devlife.connect2sql.sql.driver.helper.DriverHelperFactory
import rx.Observable
......@@ -12,32 +14,44 @@ import java.sql.DriverManager
import java.sql.SQLException
import java.util.concurrent.ConcurrentHashMap
/**
*/
class ConnectionAgent {
class ConnectionAgent(private val sshTunnelAgent: SshTunnelAgent) {
private val activeConnections = ConcurrentHashMap<ConnectionInfo, Connection>()
fun connect(connectionInfo: ConnectionInfo): Observable<Connection> {
return if (connectionInfo.sshConfig != null) {
val serviceAddress = Address(connectionInfo.host, connectionInfo.port)
val sshTunnelConfig = SshTunnelConfig(connectionInfo.sshConfig, serviceAddress)
sshTunnelAgent.startSshTunnel(sshTunnelConfig).flatMap { (_, tunnelAddress) ->
doConnect(connectionInfo, tunnelAddress)
}
} else {
doConnect(connectionInfo, null)
}
}
fun disconnect(connection: Connection): Observable<Unit> {
return Observable.create { subscriber ->
try {
if (!activeConnections.containsKey(connectionInfo) || activeConnections[connectionInfo]?.isClosed() == true) {
try {
val s = Socket()
val address = InetSocketAddress(
connectionInfo.host.split("\\\\".toRegex())[0],
connectionInfo.port)
if (!connection.isClosed) {
connection.close()
}
subscriber.onNext(Unit)
subscriber.onCompleted()
} catch (e: SQLException) {
subscriber.onError(e)
}
}
}
s.connect(address, 10000)
if (s.isConnected) {
s.close()
}
} catch (e: IllegalArgumentException) {
throw SQLException(e.message, e)
} catch (e: IOException) {
throw SQLException(e.message, e)
}
private fun doConnect(
connectionInfo: ConnectionInfo,
tunnelAddress: Address?
): Observable<Connection> {
return Observable.create { subscriber ->
try {
if (!activeConnections.containsKey(connectionInfo)
|| activeConnections[connectionInfo]?.isClosed == true) {
val driverHelper = DriverHelperFactory.create(connectionInfo.driverType)
?: throw SQLException("Driver not found for ${connectionInfo.driverType}")
......@@ -47,18 +61,44 @@ class ConnectionAgent {
EzLogger.d("Importing database driver: ${driverHelper.driverClass}")
Class.forName(driverHelper.driverClass)
// build our connection path
val connectionPath = driverHelper.createConnectionString(connectionInfo)
// determine the final connection info (if tunnel is configured)
val finalConnectionInfo = when {
tunnelAddress != null -> {
connectionInfo.copy(
host = tunnelAddress.host,
port = tunnelAddress.port
)
}
else -> connectionInfo
}
// check raw connection since DriverManager timeout appears to not be working
try {
val s = Socket()
val address = InetSocketAddress(
finalConnectionInfo.host.split("\\\\".toRegex())[0],
finalConnectionInfo.port)
s.connect(address, 10000)
if (s.isConnected) {
s.close()
}
} catch (e: IllegalArgumentException) {
throw SQLException(e.message, e)
} catch (e: IOException) {
throw SQLException(e.message, e)
}
// connect
val connectionPath = driverHelper.createConnectionString(finalConnectionInfo)
EzLogger.d("Connecting to: $connectionPath")
DriverManager.setLoginTimeout(30)
val connection = DriverManager.getConnection(
connectionPath,
connectionInfo.username,
connectionInfo.password)
finalConnectionInfo.username,
finalConnectionInfo.password)
activeConnections.put(connectionInfo, connection)
activeConnections[connectionInfo] = connection
} catch (e: ClassNotFoundException) {
throw SQLException("Class not found: " + e.message, e)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
......@@ -73,18 +113,4 @@ class ConnectionAgent {
}
}
}
fun disconnect(connection: Connection): Observable<Unit> {
return Observable.create { subscriber ->
try {
if (!connection.isClosed) {
connection.close()
}
subscriber.onNext(Unit)
subscriber.onCompleted()
} catch (e: SQLException) {
subscriber.onError(e)
}
}
}
}
package app.devlife.connect2sql.connection
import app.devlife.connect2sql.db.model.connection.Address
import app.devlife.connect2sql.db.model.connection.BasicAuth
import app.devlife.connect2sql.db.model.connection.PrivateKey
import app.devlife.connect2sql.db.model.connection.SshTunnelConfig
import com.jcraft.jsch.HostKey
import com.jcraft.jsch.JSch
import com.jcraft.jsch.JSchException
import com.jcraft.jsch.Session
import rx.Observable
import java.net.ServerSocket
import java.util.concurrent.ConcurrentHashMap
class SshTunnelAgent(private val jSch: JSch) {
private val activeTunnels = ConcurrentHashMap<SshTunnelConfig, Pair<Session, Address>>()
fun startSshTunnel(sshTunnelConfig: SshTunnelConfig): Observable<Pair<Session, Address>> {
return Observable.create { subscriber ->
try {
if (activeTunnels[sshTunnelConfig] == null || activeTunnels[sshTunnelConfig]!!.first.isConnected) {
val sshConfig = sshTunnelConfig.proxy
val serviceAddress = sshTunnelConfig.serviceAddress
val session = jSch.getSession(
sshConfig.authentication.username,
sshConfig.address.host,
sshConfig.address.port)
when (sshConfig.authentication) {
is PrivateKey -> jSch.addIdentity(
"${sshConfig.authentication.username}@${sshConfig.address.host}:${sshConfig.address.port}",
sshConfig.authentication.privateKeyContents.toByteArray(),
null,
sshConfig.authentication.passphrase?.toByteArray()
)
is BasicAuth -> session.setPassword(sshConfig.authentication.password)
}
try {
session.connect()
} catch (e: JSchException) {
throw when {
e.message?.startsWith("UnknownHostKey") == true -> {
UnknownHostException(
session.hostKey)
}
else -> e
}
}
val boundPort = session.setPortForwardingL(
LOCALHOST,
unusedPort,
serviceAddress.host,
serviceAddress.port)
activeTunnels[sshTunnelConfig] = Pair(session, Address(LOCALHOST, boundPort))
}
subscriber.onNext(activeTunnels[sshTunnelConfig])
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
subscriber.onError(e)
}
}
}
private val unusedPort: Int
get() = ServerSocket(0).let {
val port = it.localPort
it.close()
port
}
data class UnknownHostException(val hostKey: HostKey) :
Exception("Unknown host ${hostKey.host}!")
companion object {
private const val LOCALHOST = "127.0.0.1"
}
}
\ No newline at end of file
package app.devlife.connect2sql.db
import android.content.Context
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
import java.util.ArrayList
import app.devlife.connect2sql.data.LockManager
import app.devlife.connect2sql.db.model.connection.ConnectionInfoSqlModel
import app.devlife.connect2sql.db.model.query.BuiltInQuery
......@@ -15,6 +9,9 @@ import app.devlife.connect2sql.db.model.query.HistoryQuery.HistoryQuerySqlModel
import app.devlife.connect2sql.db.model.query.SavedQuery.SavedQuerySqlModel
import app.devlife.connect2sql.log.EzLogger
import app.devlife.connect2sql.sql.DriverType
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
import java.util.ArrayList
class AppDatabaseHelperV3(
context: Context,
......@@ -26,10 +23,10 @@ class AppDatabaseHelperV3(
) : SQLiteOpenHelper(context, AppDatabaseHelperV3.DB_NAME, null, AppDatabaseHelperV3.DB_VERSION) {
private val models = arrayOf(
builtInQuerySqlModel,
connectionInfoSqlModel,
historyQuerySqlModel,
savedQuerySqlModel)
builtInQuerySqlModel,
connectionInfoSqlModel,
historyQuerySqlModel,
savedQuerySqlModel)
val writableDatabase: SQLiteDatabase
@Synchronized @Throws(PassphraseNotEnteredException::class)
......@@ -66,6 +63,11 @@ class AppDatabaseHelperV3(
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
models.forEach { model ->
model.upgradeSql(oldVersion, newVersion).forEach { sql ->