1 /*
2  * Copyright 2020 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.inspection;
18 
19 import static androidx.sqlite.inspection.DatabaseExtensions.isInMemoryDatabase;
20 import static androidx.sqlite.inspection.DatabaseExtensions.pathForDatabase;
21 
22 import android.database.sqlite.SQLiteDatabase;
23 import android.util.ArraySet;
24 
25 import androidx.annotation.GuardedBy;
26 
27 import org.jspecify.annotations.NonNull;
28 import org.jspecify.annotations.Nullable;
29 
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.Map;
33 import java.util.Set;
34 
35 /**
36  * The class keeps track of databases under inspection, and can keep database connections open if
37  * such option is enabled.
38  * <p>Signals expected to be provided to the class:
39  * <ul>
40  * <li>{@link #notifyDatabaseOpened} - should be called when the inspection code detects a
41  * database open operation.
42  * <li>{@link #notifyAllDatabaseReferencesReleased} - should be called when the inspection code
43  * detects that the last database connection reference has been released (effectively a connection
44  * closed event).
45  * <li>{@link #notifyKeepOpenToggle} - should be called when the inspection code detects a
46  * request to change the keep-database-connection-open setting (enabled|disabled).
47  * </ul></p>
48  * <p>Callbacks exposed by the class:
49  * <ul>
50  * <li>Detected a database that is now open, and previously was either closed or not tracked.
51  * <li>Detected a database that is now closed, and previously was reported as open.
52  * </ul></p>
53  */
54 @SuppressWarnings({"DanglingJavadoc", "SyntheticAccessor"})
55 class DatabaseRegistry {
56     private static final int NOT_TRACKED = -1;
57 
58     // Called when tracking state changes (notTracked|closed)->open
59     private final Callback mOnOpenedCallback;
60     // Called when tracking state changes open->closed
61     private final Callback mOnClosedCallback;
62 
63     // True if keep-database-connection-open functionality is enabled.
64     private boolean mKeepDatabasesOpen = false;
65 
66     private final Object mLock = new Object();
67 
68     // Starting from '1' to distinguish from '0' which could stand for an unset parameter.
69     @GuardedBy("mLock") private int mNextId = 1;
70 
71     // TODO: decide if use weak-references to database objects
72 
73     /**
74      * Database connection id -> a list of database references pointing to the same database. The
75      * collection is meant to only contain open connections (eventually consistent after all
76      * callbacks queued behind {@link #mLock} are processed).
77      */
78     @GuardedBy("mLock") private final Map<Integer, Set<SQLiteDatabase>> mDatabases =
79             new HashMap<>();
80 
81     // Database connection id -> extra database reference used to facilitate the
82     // keep-database-connection-open functionality.
83     @GuardedBy("mLock") private final Map<Integer, KeepOpenReference> mKeepOpenReferences =
84             new HashMap<>();
85 
86     // Database path -> database connection id - allowing to report a consistent id for all
87     // references pointing to the same path.
88     @GuardedBy("mLock") private final Map<String, Integer> mPathToId = new HashMap<>();
89 
90     /**
91      * @param onOpenedCallback called when tracking state changes (notTracked|closed)->open
92      * @param onClosedCallback called when tracking state changes open->closed
93      */
DatabaseRegistry(Callback onOpenedCallback, Callback onClosedCallback)94     DatabaseRegistry(Callback onOpenedCallback, Callback onClosedCallback) {
95         mOnOpenedCallback = onOpenedCallback;
96         mOnClosedCallback = onClosedCallback;
97     }
98 
99     /**
100      * Should be called when the inspection code detects a database being open operation.
101      * <p> Note that the method should be called before any code has a chance to close the
102      * database, so e.g. in an {@link androidx.inspection.ArtTooling.ExitHook#onExit}
103      * before the return value is released.
104      * Thread-safe.
105      */
notifyDatabaseOpened(@onNull SQLiteDatabase database)106     void notifyDatabaseOpened(@NonNull SQLiteDatabase database) {
107         handleDatabaseSignal(database);
108     }
109 
notifyReleaseReference(SQLiteDatabase database)110     void notifyReleaseReference(SQLiteDatabase database) {
111         synchronized (mLock) {
112             /* Prevent all other methods from releasing a reference if a
113                {@link KeepOpenReference} is present */
114             for (KeepOpenReference reference : mKeepOpenReferences.values()) {
115                 if (reference.mDatabase == database) {
116                     /* The below will always succeed as {@link mKeepOpenReferences} only
117                      * contains active references:
118                      * - we only insert active references into {@link mKeepOpenReferences}
119                      * - {@link KeepOpenReference#releaseAllReferences} is the only place where we
120                      * allow references to be released
121                      * - {@link KeepOpenReference#releaseAllReferences} is private an can only be
122                      * called from this class; and before it is called, it must be removed from
123                      * from {@link mKeepOpenReferences}
124                      */
125                     reference.acquireReference();
126                 }
127             }
128         }
129     }
130 
131     /**
132      * Should be called when the inspection code detects that the last database connection
133      * reference has been released (effectively a connection closed event).
134      * Thread-safe.
135      */
notifyAllDatabaseReferencesReleased(@onNull SQLiteDatabase database)136     void notifyAllDatabaseReferencesReleased(@NonNull SQLiteDatabase database) {
137         handleDatabaseSignal(database);
138     }
139 
140     /**
141      * Should be called when the inspection code detects a request to change the
142      * keep-database-connection-open setting (enabled|disabled).
143      * Thread-safe.
144      */
notifyKeepOpenToggle(boolean setEnabled)145     void notifyKeepOpenToggle(boolean setEnabled) {
146         synchronized (mLock) {
147             if (mKeepDatabasesOpen == setEnabled) {
148                 return; // no change
149             }
150 
151             if (setEnabled) { // allowClose -> keepOpen
152                 mKeepDatabasesOpen = true;
153 
154                 for (int id : mDatabases.keySet()) {
155                     secureKeepOpenReference(id);
156                 }
157             } else { // keepOpen -> allowClose
158                 mKeepDatabasesOpen = false;
159 
160                 Iterator<Map.Entry<Integer, KeepOpenReference>> iterator =
161                         mKeepOpenReferences.entrySet().iterator();
162                 while (iterator.hasNext()) {
163                     KeepOpenReference reference = iterator.next().getValue();
164                     iterator.remove(); // first remove so it doesn't get in its own way
165                     reference.releaseAllReferences(); // then release its references
166                 }
167             }
168         }
169     }
170 
171     /**
172      * Should be called at the start of inspection to pre-populate the list of databases with
173      * ones on disk.
174      */
notifyOnDiskDatabase(@onNull String path)175     void notifyOnDiskDatabase(@NonNull String path) {
176         synchronized (mLock) {
177             Integer currentId = mPathToId.get(path);
178             if (currentId == null) {
179                 int id = mNextId++;
180                 mPathToId.put(path, id);
181                 mOnClosedCallback.onPostEvent(id, path);
182             }
183         }
184     }
185 
186     /** Thread-safe */
handleDatabaseSignal(@onNull SQLiteDatabase database)187     private void handleDatabaseSignal(@NonNull SQLiteDatabase database) {
188         Integer notifyOpenedId = null;
189         Integer notifyClosedId = null;
190 
191         synchronized (mLock) {
192             int id = getIdForDatabase(database);
193 
194             // TODO: revisit the text below since now we're synchronized on the same lock (mLock)
195             //  as releaseReference() calls -- which most likely allows for simplifying invariants
196             // Guaranteed up to date:
197             // - either called in a secure context (e.g. before the newly created connection is
198             // returned from the creation; or with an already acquiredReference on it),
199             // - or called after the last reference was released which cannot be undone.
200             final boolean isOpen = database.isOpen();
201 
202             if (id == NOT_TRACKED) { // handling a transition: not tracked -> tracked
203                 id = mNextId++;
204                 registerReference(id, database);
205                 if (isOpen) {
206                     notifyOpenedId = id;
207                 } else {
208                     notifyClosedId = id;
209                 }
210             } else if (isOpen) { // handling a transition: tracked(closed) -> tracked(open)
211                 // There are two scenarios here:
212                 // - hasReferences is up to date and there is an open reference already, so we
213                 // don't need to announce a new one
214                 // - hasReferences is stale, and references in it are queued up to be
215                 // announced as closing, in this case the outside world thinks that the
216                 // connection is open (close ones not processed yet), so we don't need to
217                 // announce anything; later, when processing the queued up closed events nothing
218                 // will be announced as the currently processed database will keep at least one open
219                 // connection.
220                 if (!hasReferences(id)) {
221                     notifyOpenedId = id;
222                 }
223                 registerReference(id, database);
224             } else { // handling a transition: tracked(open) -> tracked(closed)
225                 // There are two scenarios here:
226                 // - hasReferences is up to date and we can use it
227                 // - hasReferences is stale, and references in it are queued up to be
228                 // announced as closed; in this case there is no harm not announcing a closed
229                 // event now as the subsequent calls will do it if appropriate
230                 final boolean hasReferencesPre = hasReferences(id);
231                 unregisterReference(id, database);
232                 final boolean hasReferencesPost = hasReferences(id);
233                 if (hasReferencesPre && !hasReferencesPost) {
234                     notifyClosedId = id;
235                 }
236             }
237 
238             secureKeepOpenReference(id);
239 
240             // notify of changes if any
241             if (notifyOpenedId != null) {
242                 mOnOpenedCallback.onPostEvent(notifyOpenedId, pathForDatabase(database));
243             } else if (notifyClosedId != null) {
244                 mOnClosedCallback.onPostEvent(notifyClosedId, pathForDatabase(database));
245             }
246         }
247     }
248 
249     /**
250      * Returns a currently active database reference if one is available. Null otherwise.
251      * Consumer of this method must release the reference when done using it.
252      * Thread-safe
253      */
getConnection(int databaseId)254     @Nullable SQLiteDatabase getConnection(int databaseId) {
255         synchronized (mLock) {
256             return getConnectionImpl(databaseId);
257         }
258     }
259 
260     @GuardedBy("mLock")
getConnectionImpl(int databaseId)261     private SQLiteDatabase getConnectionImpl(int databaseId) {
262         KeepOpenReference keepOpenReference = mKeepOpenReferences.get(databaseId);
263         if (keepOpenReference != null) {
264             return keepOpenReference.mDatabase;
265         }
266 
267         final Set<SQLiteDatabase> references = mDatabases.get(databaseId);
268         if (references == null) return null;
269 
270         // tries to find an open reference preferring write-enabled over read-only
271         SQLiteDatabase readOnlyReference = null;
272         for (SQLiteDatabase reference : references) {
273             if (reference.isOpen()) {
274                 if (!reference.isReadOnly()) return reference; // write-enabled was found: return it
275                 readOnlyReference = reference; // remember the read-only reference but keep looking
276             }
277         }
278         return readOnlyReference; // or null if we did not find an open reference
279     }
280 
281     @GuardedBy("mLock")
registerReference(int id, @NonNull SQLiteDatabase database)282     private void registerReference(int id, @NonNull SQLiteDatabase database) {
283         Set<SQLiteDatabase> references = mDatabases.get(id);
284         if (references == null) {
285             references = new ArraySet<>(1);
286             mDatabases.put(id, references);
287             if (!isInMemoryDatabase(database)) {
288                 mPathToId.put(pathForDatabase(database), id);
289             }
290         }
291         // mDatabases only tracks open instances
292         if (database.isOpen()) {
293             references.add(database);
294         }
295     }
296 
297     @GuardedBy("mLock")
unregisterReference(int id, @NonNull SQLiteDatabase database)298     private void unregisterReference(int id, @NonNull SQLiteDatabase database) {
299         Set<SQLiteDatabase> references = mDatabases.get(id);
300         if (references == null) {
301             return;
302         }
303         references.remove(database);
304     }
305 
306     @GuardedBy("mLock")
secureKeepOpenReference(int id)307     private void secureKeepOpenReference(int id) {
308         if (!mKeepDatabasesOpen || mKeepOpenReferences.containsKey(id)) {
309             // Keep-open is disabled or we already have a keep-open-reference for that id.
310             return;
311         }
312 
313         // Try secure a keep-open reference
314         SQLiteDatabase reference = getConnectionImpl(id);
315         if (reference != null) {
316             mKeepOpenReferences.put(id, new KeepOpenReference(reference));
317         }
318     }
319 
320     @GuardedBy("mLock")
getIdForDatabase(SQLiteDatabase database)321     private int getIdForDatabase(SQLiteDatabase database) {
322         String databasePath = pathForDatabase(database);
323 
324         Integer previousId = mPathToId.get(databasePath);
325         if (previousId != null) {
326             return previousId;
327         }
328 
329         if (isInMemoryDatabase(database)) {
330             for (Map.Entry<Integer, Set<SQLiteDatabase>> entry : mDatabases.entrySet()) {
331                 for (SQLiteDatabase entryDb : entry.getValue()) {
332                     if (entryDb == database) {
333                         return entry.getKey();
334                     }
335                 }
336             }
337         }
338 
339         return NOT_TRACKED;
340     }
341 
342     @GuardedBy("mLock")
hasReferences(int databaseId)343     private boolean hasReferences(int databaseId) {
344         final Set<SQLiteDatabase> references = mDatabases.get(databaseId);
345         return references != null && !references.isEmpty();
346     }
347 
348     interface Callback {
onPostEvent(int databaseId, String path)349         void onPostEvent(int databaseId, String path);
350     }
351 
352     private static final class KeepOpenReference {
353         private final SQLiteDatabase mDatabase;
354 
355         private final Object mLock = new Object();
356         @GuardedBy("mLock") private int mAcquiredReferenceCount = 0;
357 
KeepOpenReference(SQLiteDatabase database)358         private KeepOpenReference(SQLiteDatabase database) {
359             mDatabase = database;
360         }
361 
acquireReference()362         private void acquireReference() {
363             synchronized (mLock) {
364                 if (DatabaseExtensions.tryAcquireReference(mDatabase)) {
365                     mAcquiredReferenceCount++;
366                 }
367             }
368         }
369 
370         /**
371          * This should only be called after removing the object from
372          * {@link DatabaseRegistry#mKeepOpenReferences}. Otherwise, the object will get in its
373          * own way or releasing its references.
374          */
releaseAllReferences()375         private void releaseAllReferences() {
376             synchronized (mLock) {
377                 for (; mAcquiredReferenceCount > 0; mAcquiredReferenceCount--) {
378                     mDatabase.releaseReference();
379                 }
380             }
381         }
382     }
383 }
384