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