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