mirror of
https://github.com/home-assistant/android
synced 2024-09-18 23:52:51 +00:00
Add migration fallback, improve crash handling (#996)
* Add migration fallback, improve crash handling * Send an event and post a notification when the migration fails * Mention widgets and move notification logic out of try and catch
This commit is contained in:
parent
3c9e30182f
commit
7ce1e9f5a4
|
@ -1,14 +1,22 @@
|
||||||
package io.homeassistant.companion.android.database
|
package io.homeassistant.companion.android.database
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import io.homeassistant.companion.android.R
|
||||||
|
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||||
import io.homeassistant.companion.android.database.authentication.Authentication
|
import io.homeassistant.companion.android.database.authentication.Authentication
|
||||||
import io.homeassistant.companion.android.database.authentication.AuthenticationDao
|
import io.homeassistant.companion.android.database.authentication.AuthenticationDao
|
||||||
import io.homeassistant.companion.android.database.sensor.Attribute
|
import io.homeassistant.companion.android.database.sensor.Attribute
|
||||||
|
@ -21,6 +29,7 @@ import io.homeassistant.companion.android.database.widget.StaticWidgetDao
|
||||||
import io.homeassistant.companion.android.database.widget.StaticWidgetEntity
|
import io.homeassistant.companion.android.database.widget.StaticWidgetEntity
|
||||||
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
|
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
|
||||||
import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity
|
import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
|
@ -44,17 +53,23 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DATABASE_NAME = "HomeAssistantDB"
|
private const val DATABASE_NAME = "HomeAssistantDB"
|
||||||
internal const val TAG = "AppDatabase"
|
internal const val TAG = "AppDatabase"
|
||||||
|
const val channelId = "App Database"
|
||||||
|
const val NOTIFICATION_ID = 45
|
||||||
|
lateinit var appContext: Context
|
||||||
|
lateinit var integrationRepository: IntegrationRepository
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var instance: AppDatabase? = null
|
private var instance: AppDatabase? = null
|
||||||
|
|
||||||
fun getInstance(context: Context): AppDatabase {
|
fun getInstance(context: Context): AppDatabase {
|
||||||
|
appContext = context
|
||||||
return instance ?: synchronized(this) {
|
return instance ?: synchronized(this) {
|
||||||
instance ?: buildDatabase(context).also { instance = it }
|
instance ?: buildDatabase(context).also { instance = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDatabase(context: Context): AppDatabase {
|
private fun buildDatabase(context: Context): AppDatabase {
|
||||||
|
appContext = context
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
|
.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
|
@ -70,6 +85,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
MIGRATION_9_10,
|
MIGRATION_9_10,
|
||||||
MIGRATION_10_11
|
MIGRATION_10_11
|
||||||
)
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,27 +155,42 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
val cursor = database.query("SELECT * FROM sensors")
|
val cursor = database.query("SELECT * FROM sensors")
|
||||||
val sensors = mutableListOf<ContentValues>()
|
val sensors = mutableListOf<ContentValues>()
|
||||||
if (cursor.count > 0) {
|
var migrationSuccessful = false
|
||||||
while (cursor.moveToNext()) {
|
var migrationFailed = false
|
||||||
sensors.add(ContentValues().also {
|
if (cursor.moveToFirst()) {
|
||||||
it.put("id", cursor.getString(cursor.getColumnIndex("unique_id")))
|
try {
|
||||||
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
while (cursor.moveToNext()) {
|
||||||
it.put("registered", cursor.getInt(cursor.getColumnIndex("registered")))
|
sensors.add(ContentValues().also {
|
||||||
it.put("state", "")
|
it.put("id", cursor.getString(cursor.getColumnIndex("unique_id")))
|
||||||
it.put("state_type", "")
|
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
||||||
it.put("type", "")
|
it.put(
|
||||||
it.put("icon", "")
|
"registered",
|
||||||
it.put("name", "")
|
cursor.getInt(cursor.getColumnIndex("registered"))
|
||||||
it.put("device_class", "")
|
)
|
||||||
})
|
it.put("state", "")
|
||||||
|
it.put("state_type", "")
|
||||||
|
it.put("type", "")
|
||||||
|
it.put("icon", "")
|
||||||
|
it.put("name", "")
|
||||||
|
it.put("device_class", "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
migrationSuccessful = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
migrationFailed = true
|
||||||
|
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
database.execSQL("DROP TABLE IF EXISTS `sensors`")
|
database.execSQL("DROP TABLE IF EXISTS `sensors`")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))")
|
database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))")
|
||||||
sensors.forEach {
|
if (migrationSuccessful) {
|
||||||
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
sensors.forEach {
|
||||||
|
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (migrationFailed)
|
||||||
|
notifyMigrationFailed()
|
||||||
|
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensor_attributes` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))")
|
database.execSQL("CREATE TABLE IF NOT EXISTS `sensor_attributes` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))")
|
||||||
}
|
}
|
||||||
|
@ -178,27 +209,42 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
val cursor = database.query("SELECT * FROM sensors")
|
val cursor = database.query("SELECT * FROM sensors")
|
||||||
val sensors = mutableListOf<ContentValues>()
|
val sensors = mutableListOf<ContentValues>()
|
||||||
if (cursor.count > 0) {
|
var migrationSuccessful = false
|
||||||
while (cursor.moveToNext()) {
|
var migrationFailed = false
|
||||||
sensors.add(ContentValues().also {
|
if (cursor.moveToFirst()) {
|
||||||
it.put("id", cursor.getString(cursor.getColumnIndex("id")))
|
try {
|
||||||
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
while (cursor.moveToNext()) {
|
||||||
it.put("registered", cursor.getInt(cursor.getColumnIndex("registered")))
|
sensors.add(ContentValues().also {
|
||||||
it.put("state", "")
|
it.put("id", cursor.getString(cursor.getColumnIndex("id")))
|
||||||
it.put("last_sent_state", "")
|
it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled")))
|
||||||
it.put("state_type", "")
|
it.put(
|
||||||
it.put("type", "")
|
"registered",
|
||||||
it.put("icon", "")
|
cursor.getInt(cursor.getColumnIndex("registered"))
|
||||||
it.put("name", "")
|
)
|
||||||
})
|
it.put("state", "")
|
||||||
|
it.put("last_sent_state", "")
|
||||||
|
it.put("state_type", "")
|
||||||
|
it.put("type", "")
|
||||||
|
it.put("icon", "")
|
||||||
|
it.put("name", "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
migrationSuccessful = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
migrationFailed = true
|
||||||
|
Log.e(TAG, "Unable to migrate, proceeding with recreating the table", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
database.execSQL("DROP TABLE IF EXISTS `sensors`")
|
database.execSQL("DROP TABLE IF EXISTS `sensors`")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))")
|
database.execSQL("CREATE TABLE IF NOT EXISTS `sensors` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, PRIMARY KEY(`id`))")
|
||||||
sensors.forEach {
|
if (migrationSuccessful) {
|
||||||
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
sensors.forEach {
|
||||||
|
database.insert("sensors", OnConflictStrategy.REPLACE, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (migrationFailed)
|
||||||
|
notifyMigrationFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,5 +253,41 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `sensor_settings` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL DEFAULT 'string', PRIMARY KEY(`sensor_id`, `name`))")
|
database.execSQL("CREATE TABLE IF NOT EXISTS `sensor_settings` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL DEFAULT 'string', PRIMARY KEY(`sensor_id`, `name`))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
var notificationChannel =
|
||||||
|
notificationManager?.getNotificationChannel(channelId)
|
||||||
|
if (notificationChannel == null) {
|
||||||
|
notificationChannel = NotificationChannel(
|
||||||
|
channelId, TAG, NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
|
notificationManager?.createNotificationChannel(notificationChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyMigrationFailed() {
|
||||||
|
createNotificationChannel()
|
||||||
|
val notification = NotificationCompat.Builder(appContext, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_ic_notification)
|
||||||
|
.setContentTitle(appContext.getString(R.string.database_migration_failed))
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.build()
|
||||||
|
with(NotificationManagerCompat.from(appContext)) {
|
||||||
|
notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
runBlocking {
|
||||||
|
try {
|
||||||
|
integrationRepository.fireEvent("mobile_app.migration_failed", mapOf())
|
||||||
|
Log.d(TAG, "Event sent to Home Assistant")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to send event to Home Assistant", e)
|
||||||
|
Toast.makeText(appContext, R.string.database_event_failure, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
to your home internet.</string>
|
to your home internet.</string>
|
||||||
<string name="connected_to_home_assistant">Connected to Home Assistant</string>
|
<string name="connected_to_home_assistant">Connected to Home Assistant</string>
|
||||||
<string name="create_template">Create Template</string>
|
<string name="create_template">Create Template</string>
|
||||||
|
<string name="database_migration_failed">Database migration failed, all sensor and widget data has been reset. Please re-enable your sensors and recreate your widgets.</string>
|
||||||
|
<string name="database_event_failure">Unable to send migration failure event to Home Assistant</string>
|
||||||
<string name="developer_tools">Developer Tools</string>
|
<string name="developer_tools">Developer Tools</string>
|
||||||
<string name="device_class">Device Class</string>
|
<string name="device_class">Device Class</string>
|
||||||
<string name="device_name">Device Name</string>
|
<string name="device_name">Device Name</string>
|
||||||
|
|
Loading…
Reference in a new issue