1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.work.impl 17 18 import android.content.ContentValues 19 import android.content.Context 20 import android.os.Build 21 import androidx.room.OnConflictStrategy 22 import androidx.room.RenameColumn 23 import androidx.room.migration.AutoMigrationSpec 24 import androidx.room.migration.Migration 25 import androidx.sqlite.db.SupportSQLiteDatabase 26 import androidx.work.OverwritingInputMerger 27 import androidx.work.impl.WorkDatabaseVersions.VERSION_1 28 import androidx.work.impl.WorkDatabaseVersions.VERSION_10 29 import androidx.work.impl.WorkDatabaseVersions.VERSION_11 30 import androidx.work.impl.WorkDatabaseVersions.VERSION_12 31 import androidx.work.impl.WorkDatabaseVersions.VERSION_13 32 import androidx.work.impl.WorkDatabaseVersions.VERSION_15 33 import androidx.work.impl.WorkDatabaseVersions.VERSION_16 34 import androidx.work.impl.WorkDatabaseVersions.VERSION_17 35 import androidx.work.impl.WorkDatabaseVersions.VERSION_2 36 import androidx.work.impl.WorkDatabaseVersions.VERSION_3 37 import androidx.work.impl.WorkDatabaseVersions.VERSION_4 38 import androidx.work.impl.WorkDatabaseVersions.VERSION_5 39 import androidx.work.impl.WorkDatabaseVersions.VERSION_6 40 import androidx.work.impl.WorkDatabaseVersions.VERSION_7 41 import androidx.work.impl.WorkDatabaseVersions.VERSION_8 42 import androidx.work.impl.WorkDatabaseVersions.VERSION_9 43 import androidx.work.impl.model.WorkSpec 44 import androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES 45 import androidx.work.impl.utils.PreferenceUtils 46 import androidx.work.impl.utils.migrateLegacyIdGenerator 47 48 /** Migration helpers for [androidx.work.impl.WorkDatabase]. */ 49 internal object WorkDatabaseVersions { 50 // Known WorkDatabase versions 51 const val VERSION_1 = 1 52 const val VERSION_2 = 2 53 const val VERSION_3 = 3 54 const val VERSION_4 = 4 55 const val VERSION_5 = 5 56 const val VERSION_6 = 6 57 const val VERSION_7 = 7 58 const val VERSION_8 = 8 59 const val VERSION_9 = 9 60 const val VERSION_10 = 10 61 const val VERSION_11 = 11 62 const val VERSION_12 = 12 63 const val VERSION_13 = 13 64 65 // (as well as version_13): 2.8.0-alpha01, making required_network_type, content_uri_triggers 66 // non null 67 const val VERSION_14 = 14 68 69 // renaming period_start_time to last_enqueue_time and adding period_count 70 const val VERSION_15 = 15 71 72 // adding generation column to WorkSpec and SystemIdInfo tables 73 const val VERSION_16 = 16 74 75 // made input_merger_class_name non null 76 const val VERSION_17 = 17 77 // next_schedule_time_override & next_schedule_time_override_generation were added 78 @Suppress("unused") const val VERSION_18 = 18 79 // stop_reason added 80 const val VERSION_19 = 19 81 // default value of last_enqueue_time changed to -1 82 const val VERSION_20 = 20 83 // added NetworkRequest to Constraints 84 const val VERSION_21 = 21 85 // need to reschedule jobs in workmanager's namespace, 86 // but no actual schema changes. 87 const val VERSION_22 = 22 88 } 89 90 private const val CREATE_SYSTEM_ID_INFO = 91 """ 92 CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id` 93 INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) 94 REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE ) 95 """ 96 private const val MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO = 97 """ 98 INSERT INTO SystemIdInfo(work_spec_id, system_id) 99 SELECT work_spec_id, alarm_id AS system_id FROM alarmInfo 100 """ 101 private const val PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT = 102 """ 103 UPDATE workspec SET schedule_requested_at = 0 104 WHERE state NOT IN $COMPLETED_STATES 105 AND schedule_requested_at = ${WorkSpec.SCHEDULE_NOT_REQUESTED_YET} 106 AND interval_duration <> 0 107 """ 108 private const val REMOVE_ALARM_INFO = "DROP TABLE IF EXISTS alarmInfo" 109 private const val WORKSPEC_ADD_TRIGGER_UPDATE_DELAY = 110 "ALTER TABLE workspec ADD COLUMN `trigger_content_update_delay` INTEGER NOT NULL DEFAULT -1" 111 private const val WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY = 112 "ALTER TABLE workspec ADD COLUMN `trigger_max_content_delay` INTEGER NOT NULL DEFAULT -1" 113 private const val CREATE_WORK_PROGRESS = 114 """ 115 CREATE TABLE IF NOT EXISTS `WorkProgress` (`work_spec_id` TEXT NOT NULL, `progress` 116 BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) 117 REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE ) 118 """ 119 private const val CREATE_INDEX_PERIOD_START_TIME = 120 """ 121 CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `workspec`(`period_start_time`) 122 """ 123 private const val CREATE_RUN_IN_FOREGROUND = 124 "ALTER TABLE workspec ADD COLUMN `run_in_foreground` INTEGER NOT NULL DEFAULT 0" 125 private const val CREATE_OUT_OF_QUOTA_POLICY = 126 "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0" 127 128 private const val SET_DEFAULT_NETWORK_TYPE = 129 "UPDATE workspec SET required_network_type = 0 WHERE required_network_type IS NULL " 130 131 private const val SET_DEFAULT_CONTENT_URI_TRIGGERS = 132 "UPDATE workspec SET content_uri_triggers = x'' WHERE content_uri_triggers is NULL" 133 134 private const val INITIALIZE_PERIOD_COUNTER = 135 "UPDATE workspec SET period_count = 1 WHERE last_enqueue_time <> 0 AND interval_duration <> 0" 136 137 /** 138 * Removes the `alarmInfo` table and substitutes it for a more general `SystemIdInfo` table. Adds 139 * implicit work tags for all work (a tag with the worker class name). 140 */ 141 object Migration_1_2 : Migration(VERSION_1, VERSION_2) { migratenull142 override fun migrate(db: SupportSQLiteDatabase) { 143 db.execSQL(CREATE_SYSTEM_ID_INFO) 144 db.execSQL(MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO) 145 db.execSQL(REMOVE_ALARM_INFO) 146 db.execSQL( 147 """ 148 INSERT OR IGNORE INTO worktag(tag, work_spec_id) 149 SELECT worker_class_name AS tag, id AS work_spec_id FROM workspec 150 """ 151 ) 152 } 153 } 154 155 /** Marks `SCHEDULE_REQUESTED_AT` to something other than `SCHEDULE_NOT_REQUESTED_AT`. */ 156 object Migration_3_4 : Migration(VERSION_3, VERSION_4) { migratenull157 override fun migrate(db: SupportSQLiteDatabase) { 158 if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) { 159 db.execSQL(PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT) 160 } 161 } 162 } 163 164 /** Adds the `ContentUri` delays to the WorkSpec table. */ 165 object Migration_4_5 : Migration(VERSION_4, VERSION_5) { migratenull166 override fun migrate(db: SupportSQLiteDatabase) { 167 db.execSQL(WORKSPEC_ADD_TRIGGER_UPDATE_DELAY) 168 db.execSQL(WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY) 169 } 170 } 171 172 /** Adds [androidx.work.impl.model.WorkProgress]. */ 173 object Migration_6_7 : Migration(VERSION_6, VERSION_7) { migratenull174 override fun migrate(db: SupportSQLiteDatabase) { 175 db.execSQL(CREATE_WORK_PROGRESS) 176 } 177 } 178 179 /** Adds an index on period_start_time in [WorkSpec]. */ 180 object Migration_7_8 : Migration(VERSION_7, VERSION_8) { migratenull181 override fun migrate(db: SupportSQLiteDatabase) { 182 db.execSQL(CREATE_INDEX_PERIOD_START_TIME) 183 } 184 } 185 186 /** Adds a notification_provider to the [WorkSpec]. */ 187 object Migration_8_9 : Migration(VERSION_8, VERSION_9) { migratenull188 override fun migrate(db: SupportSQLiteDatabase) { 189 db.execSQL(CREATE_RUN_IN_FOREGROUND) 190 } 191 } 192 193 /** Adds a notification_provider to the [WorkSpec]. */ 194 object Migration_11_12 : Migration(VERSION_11, VERSION_12) { migratenull195 override fun migrate(db: SupportSQLiteDatabase) { 196 db.execSQL(CREATE_OUT_OF_QUOTA_POLICY) 197 } 198 } 199 200 object Migration_12_13 : Migration(VERSION_12, VERSION_13) { migratenull201 override fun migrate(db: SupportSQLiteDatabase) { 202 db.execSQL(SET_DEFAULT_NETWORK_TYPE) 203 db.execSQL(SET_DEFAULT_CONTENT_URI_TRIGGERS) 204 } 205 } 206 207 @RenameColumn( 208 tableName = "WorkSpec", 209 fromColumnName = "period_start_time", 210 toColumnName = "last_enqueue_time" 211 ) 212 class AutoMigration_14_15 : AutoMigrationSpec { onPostMigratenull213 override fun onPostMigrate(db: SupportSQLiteDatabase) { 214 db.execSQL(INITIALIZE_PERIOD_COUNTER) 215 val values = ContentValues(1) 216 values.put("last_enqueue_time", System.currentTimeMillis()) 217 db.update( 218 "WorkSpec", 219 OnConflictStrategy.ABORT, 220 values, 221 "last_enqueue_time = 0 AND interval_duration <> 0 ", 222 emptyArray() 223 ) 224 } 225 } 226 227 /** A [WorkDatabase] migration that reschedules all eligible Workers. */ 228 class RescheduleMigration(val mContext: Context, startVersion: Int, endVersion: Int) : 229 Migration(startVersion, endVersion) { migratenull230 override fun migrate(db: SupportSQLiteDatabase) { 231 if (endVersion >= VERSION_10) { 232 db.execSQL( 233 PreferenceUtils.INSERT_PREFERENCE, 234 arrayOf(PreferenceUtils.KEY_RESCHEDULE_NEEDED, 1) 235 ) 236 } else { 237 val preferences = 238 mContext.getSharedPreferences( 239 PreferenceUtils.PREFERENCES_FILE_NAME, 240 Context.MODE_PRIVATE 241 ) 242 243 // Mutate the shared preferences directly, and eventually they will get 244 // migrated to the data store post v10. 245 preferences.edit().putBoolean(PreferenceUtils.KEY_RESCHEDULE_NEEDED, true).apply() 246 } 247 } 248 } 249 250 /** Adds the [androidx.work.impl.model.Preference] table. */ 251 internal class WorkMigration9To10(private val context: Context) : Migration(VERSION_9, VERSION_10) { migratenull252 override fun migrate(db: SupportSQLiteDatabase) { 253 db.execSQL(PreferenceUtils.CREATE_PREFERENCE) 254 PreferenceUtils.migrateLegacyPreferences(context, db) 255 migrateLegacyIdGenerator(context, db) 256 } 257 } 258 259 object Migration_15_16 : Migration(VERSION_15, VERSION_16) { migratenull260 override fun migrate(db: SupportSQLiteDatabase) { 261 // b/239543214: unclear how data got corrupted, 262 // but foreign key check on SystemIdInfo fails, 263 // meaning SystemIdInfo has work_spec_id that doesn't exist in WorkSpec table. 264 db.execSQL( 265 "DELETE FROM SystemIdInfo WHERE work_spec_id IN " + 266 "(SELECT work_spec_id FROM SystemIdInfo " + 267 "LEFT JOIN WorkSpec ON work_spec_id = id WHERE WorkSpec.id IS NULL)" 268 ) 269 270 db.execSQL("ALTER TABLE `WorkSpec` ADD COLUMN `generation` " + "INTEGER NOT NULL DEFAULT 0") 271 db.execSQL( 272 """CREATE TABLE IF NOT EXISTS `_new_SystemIdInfo` ( 273 `work_spec_id` TEXT NOT NULL, 274 `generation` INTEGER NOT NULL DEFAULT 0, 275 `system_id` INTEGER NOT NULL, 276 PRIMARY KEY(`work_spec_id`, `generation`), 277 FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) 278 ON UPDATE CASCADE ON DELETE CASCADE ) 279 """ 280 .trimIndent() 281 ) 282 db.execSQL( 283 "INSERT INTO `_new_SystemIdInfo` (`work_spec_id`,`system_id`) " + 284 "SELECT `work_spec_id`,`system_id` FROM `SystemIdInfo`" 285 ) 286 db.execSQL("DROP TABLE `SystemIdInfo`") 287 db.execSQL("ALTER TABLE `_new_SystemIdInfo` RENAME TO `SystemIdInfo`") 288 } 289 } 290 291 object Migration_16_17 : Migration(VERSION_16, VERSION_17) { migratenull292 override fun migrate(db: SupportSQLiteDatabase) { 293 // b/261721822: unclear how the content of input_merger_class_name could have been, 294 // null such that it fails to migrate to a table with a NOT NULL constrain, therefore 295 // set the current default value to avoid dropping the worker. 296 db.execSQL( 297 """UPDATE WorkSpec 298 SET input_merger_class_name = '${OverwritingInputMerger::class.java.name}' 299 WHERE input_merger_class_name IS NULL 300 """ 301 .trimIndent() 302 ) 303 db.execSQL( 304 """CREATE TABLE IF NOT EXISTS `_new_WorkSpec` ( 305 `id` TEXT NOT NULL, 306 `state` INTEGER NOT NULL, 307 `worker_class_name` TEXT NOT NULL, 308 `input_merger_class_name` TEXT NOT NULL, 309 `input` BLOB NOT NULL, 310 `output` BLOB NOT NULL, 311 `initial_delay` INTEGER NOT NULL, 312 `interval_duration` INTEGER NOT NULL, 313 `flex_duration` INTEGER NOT NULL, 314 `run_attempt_count` INTEGER NOT NULL, 315 `backoff_policy` INTEGER NOT NULL, 316 `backoff_delay_duration` INTEGER NOT NULL, 317 `last_enqueue_time` INTEGER NOT NULL, 318 `minimum_retention_duration` INTEGER NOT NULL, 319 `schedule_requested_at` INTEGER NOT NULL, 320 `run_in_foreground` INTEGER NOT NULL, 321 `out_of_quota_policy` INTEGER NOT NULL, 322 `period_count` INTEGER NOT NULL DEFAULT 0, 323 `generation` INTEGER NOT NULL DEFAULT 0, 324 `required_network_type` INTEGER NOT NULL, 325 `requires_charging` INTEGER NOT NULL, 326 `requires_device_idle` INTEGER NOT NULL, 327 `requires_battery_not_low` INTEGER NOT NULL, 328 `requires_storage_not_low` INTEGER NOT NULL, 329 `trigger_content_update_delay` INTEGER NOT NULL, 330 `trigger_max_content_delay` INTEGER NOT NULL, 331 `content_uri_triggers` BLOB NOT NULL, 332 PRIMARY KEY(`id`) 333 )""" 334 .trimIndent() 335 ) 336 db.execSQL( 337 """INSERT INTO `_new_WorkSpec` ( 338 `id`, 339 `state`, 340 `worker_class_name`, 341 `input_merger_class_name`, 342 `input`, 343 `output`, 344 `initial_delay`, 345 `interval_duration`, 346 `flex_duration`, 347 `run_attempt_count`, 348 `backoff_policy`, 349 `backoff_delay_duration`, 350 `last_enqueue_time`, 351 `minimum_retention_duration`, 352 `schedule_requested_at`, 353 `run_in_foreground`, 354 `out_of_quota_policy`, 355 `period_count`, 356 `generation`, 357 `required_network_type`, 358 `requires_charging`, 359 `requires_device_idle`, 360 `requires_battery_not_low`, 361 `requires_storage_not_low`, 362 `trigger_content_update_delay`, 363 `trigger_max_content_delay`, 364 `content_uri_triggers` 365 ) SELECT 366 `id`, 367 `state`, 368 `worker_class_name`, 369 `input_merger_class_name`, 370 `input`, 371 `output`, 372 `initial_delay`, 373 `interval_duration`, 374 `flex_duration`, 375 `run_attempt_count`, 376 `backoff_policy`, 377 `backoff_delay_duration`, 378 `last_enqueue_time`, 379 `minimum_retention_duration`, 380 `schedule_requested_at`, 381 `run_in_foreground`, 382 `out_of_quota_policy`, 383 `period_count`, 384 `generation`, 385 `required_network_type`, 386 `requires_charging`, 387 `requires_device_idle`, 388 `requires_battery_not_low`, 389 `requires_storage_not_low`, 390 `trigger_content_update_delay`, 391 `trigger_max_content_delay`, 392 `content_uri_triggers` 393 FROM `WorkSpec`""" 394 .trimIndent() 395 ) 396 db.execSQL("DROP TABLE `WorkSpec`") 397 db.execSQL("ALTER TABLE `_new_WorkSpec` RENAME TO `WorkSpec`") 398 db.execSQL( 399 "CREATE INDEX IF NOT EXISTS `index_WorkSpec_schedule_requested_at`" + 400 "ON `WorkSpec` (`schedule_requested_at`)" 401 ) 402 db.execSQL( 403 "CREATE INDEX IF NOT EXISTS `index_WorkSpec_last_enqueue_time` ON" + 404 "`WorkSpec` (`last_enqueue_time`)" 405 ) 406 } 407 } 408 409 class AutoMigration_19_20 : AutoMigrationSpec { onPostMigratenull410 override fun onPostMigrate(db: SupportSQLiteDatabase) { 411 db.execSQL("UPDATE WorkSpec SET `last_enqueue_time` = -1 WHERE `last_enqueue_time` = 0") 412 } 413 } 414