1 /*
2  * Copyright (C) 2011 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 android.database.sqlite;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 import android.text.TextUtils;
21 import android.util.ArrayMap;
22 import android.util.Pair;
23 import java.util.ArrayList;
24 import java.util.Locale;
25 import java.util.function.BinaryOperator;
26 import java.util.function.UnaryOperator;
27 import java.util.regex.Pattern;
28 
29 /**
30  * Describes how to configure a database.
31  * <p>
32  * The purpose of this object is to keep track of all of the little
33  * configuration settings that are applied to a database after it
34  * is opened so that they can be applied to all connections in the
35  * connection pool uniformly.
36  * </p><p>
37  * Each connection maintains its own copy of this object so it can
38  * keep track of which settings have already been applied.
39  * </p>
40  *
41  * @hide
42  */
43 public final class SQLiteDatabaseConfiguration {
44     // The pattern we use to strip email addresses from database paths
45     // when constructing a label to use in log messages.
46     private static final Pattern EMAIL_IN_DB_PATTERN =
47             Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
48 
49     /**
50      * Special path used by in-memory databases.
51      */
52     public static final String MEMORY_DB_PATH = ":memory:";
53 
54     /**
55      * The database path.
56      */
57     public final String path;
58 
59     /**
60      * The label to use to describe the database when it appears in logs.
61      * This is derived from the path but is stripped to remove PII.
62      */
63     public final String label;
64 
65     /**
66      * The flags used to open the database.
67      */
68     public int openFlags;
69 
70     /**
71      * The maximum size of the prepared statement cache for each database connection.
72      * Must be non-negative.
73      *
74      * Default is 25.
75      */
76     @UnsupportedAppUsage
77     public int maxSqlCacheSize;
78 
79     /**
80      * The database locale.
81      *
82      * Default is the value returned by {@link Locale#getDefault()}.
83      */
84     public Locale locale;
85 
86     /**
87      * True if foreign key constraints are enabled.
88      *
89      * Default is false.
90      */
91     public boolean foreignKeyConstraintsEnabled;
92 
93     /**
94      * The custom scalar functions to register.
95      */
96     public final ArrayMap<String, UnaryOperator<String>> customScalarFunctions
97             = new ArrayMap<>();
98 
99     /**
100      * The custom aggregate functions to register.
101      */
102     public final ArrayMap<String, BinaryOperator<String>> customAggregateFunctions
103             = new ArrayMap<>();
104 
105     /**
106      * The statements to execute to initialize each connection.
107      */
108     public final ArrayList<Pair<String, Object[]>> perConnectionSql = new ArrayList<>();
109 
110     /**
111      * The size in bytes of each lookaside slot
112      *
113      * <p>If negative, the default lookaside configuration will be used
114      */
115     public int lookasideSlotSize = -1;
116 
117     /**
118      * The total number of lookaside memory slots per database connection
119      *
120      * <p>If negative, the default lookaside configuration will be used
121      */
122     public int lookasideSlotCount = -1;
123 
124     /**
125      * The number of milliseconds that SQLite connection is allowed to be idle before it
126      * is closed and removed from the pool.
127      * <p>By default, idle connections are not closed
128      */
129     public long idleConnectionTimeoutMs = Long.MAX_VALUE;
130 
131     /**
132      * Journal mode to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} is not set.
133      * <p>Default is returned by {@link SQLiteGlobal#getDefaultJournalMode()}
134      */
135     public @SQLiteDatabase.JournalMode String journalMode;
136 
137     /**
138      * Synchronous mode to use.
139      * <p>Default is returned by {@link SQLiteGlobal#getDefaultSyncMode()}
140      * or {@link SQLiteGlobal#getWALSyncMode()} depending on journal mode
141      */
142     public @SQLiteDatabase.SyncMode String syncMode;
143 
144     public boolean shouldTruncateWalFile;
145 
146     /**
147      * Creates a database configuration with the required parameters for opening a
148      * database and default values for all other parameters.
149      *
150      * @param path The database path.
151      * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}.
152      */
SQLiteDatabaseConfiguration(String path, int openFlags)153     public SQLiteDatabaseConfiguration(String path, int openFlags) {
154         if (path == null) {
155             throw new IllegalArgumentException("path must not be null.");
156         }
157 
158         this.path = path;
159         label = stripPathForLogs(path);
160         this.openFlags = openFlags;
161 
162         // Set default values for optional parameters.
163         maxSqlCacheSize = 25;
164         locale = Locale.getDefault();
165     }
166 
167     /**
168      * Creates a database configuration as a copy of another configuration.
169      *
170      * @param other The other configuration.
171      */
SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other)172     public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) {
173         if (other == null) {
174             throw new IllegalArgumentException("other must not be null.");
175         }
176 
177         this.path = other.path;
178         this.label = other.label;
179         updateParametersFrom(other);
180     }
181 
182     /**
183      * Updates the non-immutable parameters of this configuration object
184      * from the other configuration object.
185      *
186      * @param other The object from which to copy the parameters.
187      */
updateParametersFrom(SQLiteDatabaseConfiguration other)188     public void updateParametersFrom(SQLiteDatabaseConfiguration other) {
189         if (other == null) {
190             throw new IllegalArgumentException("other must not be null.");
191         }
192         if (!path.equals(other.path)) {
193             throw new IllegalArgumentException("other configuration must refer to "
194                     + "the same database.");
195         }
196 
197         openFlags = other.openFlags;
198         maxSqlCacheSize = other.maxSqlCacheSize;
199         locale = other.locale;
200         foreignKeyConstraintsEnabled = other.foreignKeyConstraintsEnabled;
201         customScalarFunctions.clear();
202         customScalarFunctions.putAll(other.customScalarFunctions);
203         customAggregateFunctions.clear();
204         customAggregateFunctions.putAll(other.customAggregateFunctions);
205         perConnectionSql.clear();
206         perConnectionSql.addAll(other.perConnectionSql);
207         lookasideSlotSize = other.lookasideSlotSize;
208         lookasideSlotCount = other.lookasideSlotCount;
209         idleConnectionTimeoutMs = other.idleConnectionTimeoutMs;
210         journalMode = other.journalMode;
211         syncMode = other.syncMode;
212     }
213 
214     /**
215      * Returns true if the database is in-memory.
216      * @return True if the database is in-memory.
217      */
isInMemoryDb()218     public boolean isInMemoryDb() {
219         return path.equalsIgnoreCase(MEMORY_DB_PATH);
220     }
221 
isReadOnlyDatabase()222     public boolean isReadOnlyDatabase() {
223         return (openFlags & SQLiteDatabase.OPEN_READONLY) != 0;
224     }
225 
isLegacyCompatibilityWalEnabled()226     boolean isLegacyCompatibilityWalEnabled() {
227         return journalMode == null && syncMode == null
228                 && (openFlags & SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL) != 0;
229     }
230 
stripPathForLogs(String path)231     private static String stripPathForLogs(String path) {
232         if (path.indexOf('@') == -1) {
233             return path;
234         }
235         return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY");
236     }
237 
isLookasideConfigSet()238     boolean isLookasideConfigSet() {
239         return lookasideSlotCount >= 0 && lookasideSlotSize >= 0;
240     }
241 
242     /**
243      * Resolves the journal mode that should be used when opening a connection to the database.
244      *
245      * Note: assumes openFlags have already been set.
246      *
247      * @return Resolved journal mode that should be used for this database connection or an empty
248      * string if no journal mode should be set.
249      */
resolveJournalMode()250     public @SQLiteDatabase.JournalMode String resolveJournalMode() {
251         if (isReadOnlyDatabase()) {
252             // No need to specify a journal mode when only reading.
253             return "";
254         }
255 
256         if (isInMemoryDb()) {
257             if (journalMode != null
258                     && journalMode.equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_OFF)) {
259                 return SQLiteDatabase.JOURNAL_MODE_OFF;
260             }
261             return SQLiteDatabase.JOURNAL_MODE_MEMORY;
262         }
263 
264         shouldTruncateWalFile = false;
265 
266         if (isWalEnabledInternal()) {
267             shouldTruncateWalFile = true;
268             return SQLiteDatabase.JOURNAL_MODE_WAL;
269         } else {
270             // WAL is not explicitly set so use requested journal mode or platform default
271             return this.journalMode != null ? this.journalMode
272                                             : SQLiteGlobal.getDefaultJournalMode();
273         }
274     }
275 
276     /**
277      * Resolves the sync mode that should be used when opening a connection to the database.
278      *
279      * Note: assumes openFlags have already been set.
280      * @return Resolved journal mode that should be used for this database connection or null
281      * if no journal mode should be set.
282      */
resolveSyncMode()283     public @SQLiteDatabase.SyncMode String resolveSyncMode() {
284         if (isReadOnlyDatabase()) {
285             // No sync mode will be used since database will be only used for reading.
286             return "";
287         }
288 
289         if (isInMemoryDb()) {
290             // No sync mode will be used since database will be in volatile memory
291             return "";
292         }
293 
294         if (!TextUtils.isEmpty(syncMode)) {
295             return syncMode;
296         }
297 
298         if (isWalEnabledInternal()) {
299             if (isLegacyCompatibilityWalEnabled()) {
300                 return SQLiteCompatibilityWalFlags.getWALSyncMode();
301             } else {
302                 return SQLiteGlobal.getWALSyncMode();
303             }
304         } else {
305             return SQLiteGlobal.getDefaultSyncMode();
306         }
307     }
308 
isWalEnabledInternal()309     private boolean isWalEnabledInternal() {
310         final boolean walEnabled = (openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0;
311         // Use compatibility WAL unless an app explicitly set journal/synchronous mode
312         // or DISABLE_COMPATIBILITY_WAL flag is set
313         final boolean isCompatibilityWalEnabled = isLegacyCompatibilityWalEnabled();
314         return walEnabled || isCompatibilityWalEnabled
315                 || (journalMode != null
316                         && journalMode.equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_WAL));
317     }
318 }
319