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 
17 package androidx.work.impl.utils;
18 
19 import static android.content.Context.MODE_PRIVATE;
20 
21 import android.content.Context;
22 import android.content.SharedPreferences;
23 
24 import androidx.annotation.RestrictTo;
25 import androidx.lifecycle.LiveData;
26 import androidx.lifecycle.Transformations;
27 import androidx.sqlite.db.SupportSQLiteDatabase;
28 import androidx.work.impl.WorkDatabase;
29 import androidx.work.impl.model.Preference;
30 
31 import org.jspecify.annotations.NonNull;
32 
33 /**
34  * Preference Utils for WorkManager.
35  *
36  */
37 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
38 public class PreferenceUtils {
39     public static final String INSERT_PREFERENCE =
40             "INSERT OR REPLACE INTO `Preference`"
41                     + " (`key`, `long_value`) VALUES"
42                     + " (@key, @long_value)";
43 
44     public static final String CREATE_PREFERENCE =
45             "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
46                     + "PRIMARY KEY(`key`))";
47 
48     // For migration
49     public static final String PREFERENCES_FILE_NAME = "androidx.work.util.preferences";
50     public static final String KEY_LAST_CANCEL_ALL_TIME_MS = "last_cancel_all_time_ms";
51     public static final String KEY_RESCHEDULE_NEEDED = "reschedule_needed";
52     private static final String KEY_LAST_FORCE_STOP_MS = "last_force_stop_ms";
53 
54     private final WorkDatabase mWorkDatabase;
55 
PreferenceUtils(@onNull WorkDatabase workDatabase)56     public PreferenceUtils(@NonNull WorkDatabase workDatabase) {
57         mWorkDatabase = workDatabase;
58     }
59 
60     /**
61      * @return The last time (in milliseconds) a {@code cancelAll} method was called
62      */
getLastCancelAllTimeMillis()63     public long getLastCancelAllTimeMillis() {
64         Long value =
65                 mWorkDatabase.preferenceDao().getLongValue(KEY_LAST_CANCEL_ALL_TIME_MS);
66 
67         return value != null ? value : 0L;
68     }
69 
70     /**
71      * @return A {@link LiveData} of the last time (in milliseconds) a {@code cancelAll} method was
72      *         called
73      */
getLastCancelAllTimeMillisLiveData()74     public @NonNull LiveData<Long> getLastCancelAllTimeMillisLiveData() {
75         LiveData<Long> observableValue =
76                 mWorkDatabase.preferenceDao().getObservableLongValue(KEY_LAST_CANCEL_ALL_TIME_MS);
77         return Transformations.map(observableValue, (Long value) -> value != null ? value : 0L);
78     }
79 
80     /**
81      * Sets the last time a {@code cancelAll} method was called
82      *
83      * @param timeMillis The time a {@code cancelAll} method was called (in milliseconds)
84      */
setLastCancelAllTimeMillis(final long timeMillis)85     public void setLastCancelAllTimeMillis(final long timeMillis) {
86         Preference preference = new Preference(KEY_LAST_CANCEL_ALL_TIME_MS, timeMillis);
87         mWorkDatabase.preferenceDao().insertPreference(preference);
88     }
89 
90     /**
91      * @return {@code true} When we should reschedule workers.
92      */
getNeedsReschedule()93     public boolean getNeedsReschedule() {
94         // This preference is being set by a Room Migration.
95         Long value = mWorkDatabase.preferenceDao().getLongValue(KEY_RESCHEDULE_NEEDED);
96         return value != null && value == 1L;
97     }
98 
99     /**
100      * Updates the key which indicates that we have rescheduled jobs.
101      */
setNeedsReschedule(boolean needsReschedule)102     public void setNeedsReschedule(boolean needsReschedule) {
103         Preference preference = new Preference(KEY_RESCHEDULE_NEEDED, needsReschedule);
104         mWorkDatabase.preferenceDao().insertPreference(preference);
105     }
106 
107     /**
108      * Updates the key which indicates the last force-stop timestamp handled by
109      * {@link androidx.work.WorkManager}.
110      */
setLastForceStopEventMillis(long lastForceStopTimeMillis)111     public void setLastForceStopEventMillis(long lastForceStopTimeMillis) {
112         Preference preference = new Preference(KEY_LAST_FORCE_STOP_MS, lastForceStopTimeMillis);
113         mWorkDatabase.preferenceDao().insertPreference(preference);
114     }
115 
116     /**
117      * Gets the timestamp for the last known force-stop event that
118      * {@link androidx.work.WorkManager} is aware of.
119      */
getLastForceStopEventMillis()120     public long getLastForceStopEventMillis() {
121         Long timestamp = mWorkDatabase.preferenceDao().getLongValue(KEY_LAST_FORCE_STOP_MS);
122         if (timestamp != null) {
123             return timestamp;
124         } else {
125             return 0;
126         }
127     }
128 
129     /**
130      * Migrates preferences from {@link android.content.SharedPreferences} to the
131      * {@link WorkDatabase}.
132      *
133      * @param context The application {@link Context}
134      */
migrateLegacyPreferences( @onNull Context context, @NonNull SupportSQLiteDatabase sqLiteDatabase)135     public static void migrateLegacyPreferences(
136             @NonNull Context context,
137             @NonNull SupportSQLiteDatabase sqLiteDatabase) {
138 
139         SharedPreferences sharedPreferences =
140                 context.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE);
141 
142         // Check to see if we have not migrated already.
143         if (sharedPreferences.contains(KEY_RESCHEDULE_NEEDED)
144                 || sharedPreferences.contains(KEY_LAST_CANCEL_ALL_TIME_MS)) {
145 
146             long lastCancelTimeMillis = sharedPreferences.getLong(KEY_LAST_CANCEL_ALL_TIME_MS, 0L);
147             boolean needsReschedule = sharedPreferences.getBoolean(KEY_RESCHEDULE_NEEDED, false);
148             long reschedule = needsReschedule ? 1L : 0L;
149             sqLiteDatabase.beginTransaction();
150             try {
151 
152                 sqLiteDatabase.execSQL(INSERT_PREFERENCE,
153                         new Object[] {KEY_LAST_CANCEL_ALL_TIME_MS, lastCancelTimeMillis});
154 
155                 sqLiteDatabase.execSQL(INSERT_PREFERENCE,
156                         new Object[] {KEY_RESCHEDULE_NEEDED, reschedule});
157 
158                 // Delete
159                 sharedPreferences.edit()
160                         .clear()
161                         .apply();
162 
163                 sqLiteDatabase.setTransactionSuccessful();
164             } finally {
165                 sqLiteDatabase.endTransaction();
166             }
167         }
168     }
169 }
170