From 7ce1e9f5a4ea79f73ddeb8309c2d0255a9edaef2 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sun, 4 Oct 2020 16:57:01 -0700 Subject: [PATCH] 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 --- .../companion/android/database/AppDatabase.kt | 142 ++++++++++++++---- app/src/main/res/values/strings.xml | 2 + 2 files changed, 114 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt b/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt index de3762a8b..11a5387f4 100644 --- a/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt +++ b/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt @@ -1,14 +1,22 @@ package io.homeassistant.companion.android.database +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.ContentValues import android.content.Context +import android.os.Build 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.OnConflictStrategy import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration 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.AuthenticationDao 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.TemplateWidgetDao import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity +import kotlinx.coroutines.runBlocking @Database( entities = [ @@ -44,17 +53,23 @@ abstract class AppDatabase : RoomDatabase() { companion object { private const val DATABASE_NAME = "HomeAssistantDB" internal const val TAG = "AppDatabase" + const val channelId = "App Database" + const val NOTIFICATION_ID = 45 + lateinit var appContext: Context + lateinit var integrationRepository: IntegrationRepository @Volatile private var instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase { + appContext = context return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } } private fun buildDatabase(context: Context): AppDatabase { + appContext = context return Room .databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .allowMainThreadQueries() @@ -70,6 +85,7 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_9_10, MIGRATION_10_11 ) + .fallbackToDestructiveMigration() .build() } @@ -139,27 +155,42 @@ abstract class AppDatabase : RoomDatabase() { override fun migrate(database: SupportSQLiteDatabase) { val cursor = database.query("SELECT * FROM sensors") val sensors = mutableListOf() - if (cursor.count > 0) { - while (cursor.moveToNext()) { - sensors.add(ContentValues().also { - it.put("id", cursor.getString(cursor.getColumnIndex("unique_id"))) - it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) - it.put("registered", cursor.getInt(cursor.getColumnIndex("registered"))) - it.put("state", "") - it.put("state_type", "") - it.put("type", "") - it.put("icon", "") - it.put("name", "") - it.put("device_class", "") - }) + var migrationSuccessful = false + var migrationFailed = false + if (cursor.moveToFirst()) { + try { + while (cursor.moveToNext()) { + sensors.add(ContentValues().also { + it.put("id", cursor.getString(cursor.getColumnIndex("unique_id"))) + it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) + it.put( + "registered", + cursor.getInt(cursor.getColumnIndex("registered")) + ) + 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() 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`))") - sensors.forEach { - database.insert("sensors", OnConflictStrategy.REPLACE, it) + if (migrationSuccessful) { + 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`))") } @@ -178,27 +209,42 @@ abstract class AppDatabase : RoomDatabase() { override fun migrate(database: SupportSQLiteDatabase) { val cursor = database.query("SELECT * FROM sensors") val sensors = mutableListOf() - if (cursor.count > 0) { - while (cursor.moveToNext()) { - sensors.add(ContentValues().also { - it.put("id", cursor.getString(cursor.getColumnIndex("id"))) - it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) - it.put("registered", cursor.getInt(cursor.getColumnIndex("registered"))) - it.put("state", "") - it.put("last_sent_state", "") - it.put("state_type", "") - it.put("type", "") - it.put("icon", "") - it.put("name", "") - }) + var migrationSuccessful = false + var migrationFailed = false + if (cursor.moveToFirst()) { + try { + while (cursor.moveToNext()) { + sensors.add(ContentValues().also { + it.put("id", cursor.getString(cursor.getColumnIndex("id"))) + it.put("enabled", cursor.getInt(cursor.getColumnIndex("enabled"))) + it.put( + "registered", + cursor.getInt(cursor.getColumnIndex("registered")) + ) + 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() 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`))") - sensors.forEach { - database.insert("sensors", OnConflictStrategy.REPLACE, it) + if (migrationSuccessful) { + 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`))") } } + + 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() + } + } + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94eb6ffb4..726051c67 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,6 +71,8 @@ to your home internet. Connected to Home Assistant Create Template + Database migration failed, all sensor and widget data has been reset. Please re-enable your sensors and recreate your widgets. + Unable to send migration failure event to Home Assistant Developer Tools Device Class Device Name