1 /*
2  * Copyright (C) 2016 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.sqlite.db;
18 
19 import android.content.Context;
20 import android.database.sqlite.SQLiteDatabase;
21 import android.database.sqlite.SQLiteException;
22 import android.os.Build;
23 import android.util.Log;
24 import android.util.Pair;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.annotation.RequiresApi;
29 
30 import java.io.File;
31 import java.io.IOException;
32 import java.util.List;
33 
34 /**
35  * An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}.
36  * Note that since that class requires overriding certain methods, support implementation
37  * uses {@link Factory#create(Configuration)} to create this and {@link Callback} to implement
38  * the methods that should be overridden.
39  */
40 @SuppressWarnings("unused")
41 public interface SupportSQLiteOpenHelper {
42     /**
43      * Return the name of the SQLite database being opened, as given to
44      * the constructor.
45      */
getDatabaseName()46     String getDatabaseName();
47 
48     /**
49      * Enables or disables the use of write-ahead logging for the database.
50      *
51      * Write-ahead logging cannot be used with read-only databases so the value of
52      * this flag is ignored if the database is opened read-only.
53      *
54      * @param enabled True if write-ahead logging should be enabled, false if it
55      *                should be disabled.
56      * @see SupportSQLiteDatabase#enableWriteAheadLogging()
57      */
58     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
setWriteAheadLoggingEnabled(boolean enabled)59     void setWriteAheadLoggingEnabled(boolean enabled);
60 
61     /**
62      * Create and/or open a database that will be used for reading and writing.
63      * The first time this is called, the database will be opened and
64      * {@link Callback#onCreate}, {@link Callback#onUpgrade} and/or {@link Callback#onOpen} will be
65      * called.
66      *
67      * <p>Once opened successfully, the database is cached, so you can
68      * call this method every time you need to write to the database.
69      * (Make sure to call {@link #close} when you no longer need the database.)
70      * Errors such as bad permissions or a full disk may cause this method
71      * to fail, but future attempts may succeed if the problem is fixed.</p>
72      *
73      * <p class="caution">Database upgrade may take a long time, you
74      * should not call this method from the application main thread, including
75      * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
76      *
77      * @return a read/write database object valid until {@link #close} is called
78      * @throws SQLiteException if the database cannot be opened for writing
79      */
getWritableDatabase()80     SupportSQLiteDatabase getWritableDatabase();
81 
82     /**
83      * Create and/or open a database.  This will be the same object returned by
84      * {@link #getWritableDatabase} unless some problem, such as a full disk,
85      * requires the database to be opened read-only.  In that case, a read-only
86      * database object will be returned.  If the problem is fixed, a future call
87      * to {@link #getWritableDatabase} may succeed, in which case the read-only
88      * database object will be closed and the read/write object will be returned
89      * in the future.
90      *
91      * <p class="caution">Like {@link #getWritableDatabase}, this method may
92      * take a long time to return, so you should not call it from the
93      * application main thread, including from
94      * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
95      *
96      * @return a database object valid until {@link #getWritableDatabase}
97      * or {@link #close} is called.
98      * @throws SQLiteException if the database cannot be opened
99      */
getReadableDatabase()100     SupportSQLiteDatabase getReadableDatabase();
101 
102     /**
103      * Close any open database object.
104      */
close()105     void close();
106 
107     /**
108      * Handles various lifecycle events for the SQLite connection, similar to
109      * {@link android.database.sqlite.SQLiteOpenHelper}.
110      */
111     @SuppressWarnings({"unused", "WeakerAccess"})
112     abstract class Callback {
113         private static final String TAG = "SupportSQLite";
114         /**
115          * Version number of the database (starting at 1); if the database is older,
116          * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)}
117          * will be used to upgrade the database; if the database is newer,
118          * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)}
119          * will be used to downgrade the database.
120          */
121         public final int version;
122 
123         /**
124          * Creates a new Callback to get database lifecycle events.
125          * @param version The version for the database instance. See {@link #version}.
126          */
Callback(int version)127         public Callback(int version) {
128             this.version = version;
129         }
130 
131         /**
132          * Called when the database connection is being configured, to enable features such as
133          * write-ahead logging or foreign key support.
134          * <p>
135          * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade},
136          * or {@link #onOpen} are called. It should not modify the database except to configure the
137          * database connection as required.
138          * </p>
139          * <p>
140          * This method should only call methods that configure the parameters of the database
141          * connection, such as {@link SupportSQLiteDatabase#enableWriteAheadLogging}
142          * {@link SupportSQLiteDatabase#setForeignKeyConstraintsEnabled},
143          * {@link SupportSQLiteDatabase#setLocale},
144          * {@link SupportSQLiteDatabase#setMaximumSize}, or executing PRAGMA statements.
145          * </p>
146          *
147          * @param db The database.
148          */
onConfigure(SupportSQLiteDatabase db)149         public void onConfigure(SupportSQLiteDatabase db) {
150 
151         }
152 
153         /**
154          * Called when the database is created for the first time. This is where the
155          * creation of tables and the initial population of the tables should happen.
156          *
157          * @param db The database.
158          */
onCreate(SupportSQLiteDatabase db)159         public abstract void onCreate(SupportSQLiteDatabase db);
160 
161         /**
162          * Called when the database needs to be upgraded. The implementation
163          * should use this method to drop tables, add tables, or do anything else it
164          * needs to upgrade to the new schema version.
165          *
166          * <p>
167          * The SQLite ALTER TABLE documentation can be found
168          * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
169          * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
170          * you can use ALTER TABLE to rename the old table, then create the new table and then
171          * populate the new table with the contents of the old table.
172          * </p><p>
173          * This method executes within a transaction.  If an exception is thrown, all changes
174          * will automatically be rolled back.
175          * </p>
176          *
177          * @param db         The database.
178          * @param oldVersion The old database version.
179          * @param newVersion The new database version.
180          */
onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion)181         public abstract void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion);
182 
183         /**
184          * Called when the database needs to be downgraded. This is strictly similar to
185          * {@link #onUpgrade} method, but is called whenever current version is newer than requested
186          * one.
187          * However, this method is not abstract, so it is not mandatory for a customer to
188          * implement it. If not overridden, default implementation will reject downgrade and
189          * throws SQLiteException
190          *
191          * <p>
192          * This method executes within a transaction.  If an exception is thrown, all changes
193          * will automatically be rolled back.
194          * </p>
195          *
196          * @param db         The database.
197          * @param oldVersion The old database version.
198          * @param newVersion The new database version.
199          */
onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion)200         public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
201             throw new SQLiteException("Can't downgrade database from version "
202                     + oldVersion + " to " + newVersion);
203         }
204 
205         /**
206          * Called when the database has been opened.  The implementation
207          * should check {@link SupportSQLiteDatabase#isReadOnly} before updating the
208          * database.
209          * <p>
210          * This method is called after the database connection has been configured
211          * and after the database schema has been created, upgraded or downgraded as necessary.
212          * If the database connection must be configured in some way before the schema
213          * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead.
214          * </p>
215          *
216          * @param db The database.
217          */
onOpen(SupportSQLiteDatabase db)218         public void onOpen(SupportSQLiteDatabase db) {
219 
220         }
221 
222         /**
223          * The method invoked when database corruption is detected. Default implementation will
224          * delete the database file.
225          *
226          * @param db the {@link SupportSQLiteDatabase} object representing the database on which
227          *           corruption is detected.
228          */
onCorruption(SupportSQLiteDatabase db)229         public void onCorruption(SupportSQLiteDatabase db) {
230             // the following implementation is taken from {@link DefaultDatabaseErrorHandler}.
231 
232             Log.e(TAG, "Corruption reported by sqlite on database: " + db.getPath());
233             // is the corruption detected even before database could be 'opened'?
234             if (!db.isOpen()) {
235                 // database files are not even openable. delete this database file.
236                 // NOTE if the database has attached databases, then any of them could be corrupt.
237                 // and not deleting all of them could cause corrupted database file to remain and
238                 // make the application crash on database open operation. To avoid this problem,
239                 // the application should provide its own {@link DatabaseErrorHandler} impl class
240                 // to delete ALL files of the database (including the attached databases).
241                 deleteDatabaseFile(db.getPath());
242                 return;
243             }
244 
245             List<Pair<String, String>> attachedDbs = null;
246             try {
247                 // Close the database, which will cause subsequent operations to fail.
248                 // before that, get the attached database list first.
249                 try {
250                     attachedDbs = db.getAttachedDbs();
251                 } catch (SQLiteException e) {
252                 /* ignore */
253                 }
254                 try {
255                     db.close();
256                 } catch (IOException e) {
257                 /* ignore */
258                 }
259             } finally {
260                 // Delete all files of this corrupt database and/or attached databases
261                 if (attachedDbs != null) {
262                     for (Pair<String, String> p : attachedDbs) {
263                         deleteDatabaseFile(p.second);
264                     }
265                 } else {
266                     // attachedDbs = null is possible when the database is so corrupt that even
267                     // "PRAGMA database_list;" also fails. delete the main database file
268                     deleteDatabaseFile(db.getPath());
269                 }
270             }
271         }
272 
deleteDatabaseFile(String fileName)273         private void deleteDatabaseFile(String fileName) {
274             if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
275                 return;
276             }
277             Log.w(TAG, "deleting the database file: " + fileName);
278             try {
279                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
280                     SQLiteDatabase.deleteDatabase(new File(fileName));
281                 } else {
282                     try {
283                         final boolean deleted = new File(fileName).delete();
284                         if (!deleted) {
285                             Log.e(TAG, "Could not delete the database file " + fileName);
286                         }
287                     } catch (Exception error) {
288                         Log.e(TAG, "error while deleting corrupted database file", error);
289                     }
290                 }
291             } catch (Exception e) {
292             /* print warning and ignore exception */
293                 Log.w(TAG, "delete failed: ", e);
294             }
295         }
296     }
297 
298     /**
299      * The configuration to create an SQLite open helper object using {@link Factory}.
300      */
301     @SuppressWarnings("WeakerAccess")
302     class Configuration {
303         /**
304          * Context to use to open or create the database.
305          */
306         @NonNull
307         public final Context context;
308         /**
309          * Name of the database file, or null for an in-memory database.
310          */
311         @Nullable
312         public final String name;
313         /**
314          * The callback class to handle creation, upgrade and downgrade.
315          */
316         @NonNull
317         public final SupportSQLiteOpenHelper.Callback callback;
318 
Configuration(@onNull Context context, @Nullable String name, @NonNull Callback callback)319         Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) {
320             this.context = context;
321             this.name = name;
322             this.callback = callback;
323         }
324 
325         /**
326          * Creates a new Configuration.Builder to create an instance of Configuration.
327          *
328          * @param context to use to open or create the database.
329          */
builder(Context context)330         public static Builder builder(Context context) {
331             return new Builder(context);
332         }
333 
334         /**
335          * Builder class for {@link Configuration}.
336          */
337         public static class Builder {
338             Context mContext;
339             String mName;
340             SupportSQLiteOpenHelper.Callback mCallback;
341 
build()342             public Configuration build() {
343                 if (mCallback == null) {
344                     throw new IllegalArgumentException("Must set a callback to create the"
345                             + " configuration.");
346                 }
347                 if (mContext == null) {
348                     throw new IllegalArgumentException("Must set a non-null context to create"
349                             + " the configuration.");
350                 }
351                 return new Configuration(mContext, mName, mCallback);
352             }
353 
Builder(@onNull Context context)354             Builder(@NonNull Context context) {
355                 mContext = context;
356             }
357 
358             /**
359              * @param name Name of the database file, or null for an in-memory database.
360              * @return This
361              */
name(@ullable String name)362             public Builder name(@Nullable String name) {
363                 mName = name;
364                 return this;
365             }
366 
367             /**
368              * @param callback The callback class to handle creation, upgrade and downgrade.
369              * @return this
370              */
callback(@onNull Callback callback)371             public Builder callback(@NonNull Callback callback) {
372                 mCallback = callback;
373                 return this;
374             }
375         }
376     }
377 
378     /**
379      * Factory class to create instances of {@link SupportSQLiteOpenHelper} using
380      * {@link Configuration}.
381      */
382     interface Factory {
383         /**
384          * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration.
385          *
386          * @param configuration The configuration to use while creating the open helper.
387          *
388          * @return A SupportSQLiteOpenHelper which can be used to open a database.
389          */
create(Configuration configuration)390         SupportSQLiteOpenHelper create(Configuration configuration);
391     }
392 }
393