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 android.database.sqlite.SQLiteDatabase;
20 
21 import org.jspecify.annotations.NonNull;
22 
23 import java.io.File;
24 import java.util.Objects;
25 
26 final class DatabaseExtensions {
27     private static final String sInMemoryDatabasePath = ":memory:";
28 
29     /** Placeholder {@code %x} is for database's hashcode */
30     private static final String sInMemoryDatabaseNameFormat =
31             sInMemoryDatabasePath + " {hashcode=0x%x}";
32 
DatabaseExtensions()33     private DatabaseExtensions() { }
34 
35     /** Thread-safe as {@link SQLiteDatabase#getPath} and {@link Object#hashCode) are thread-safe.*/
pathForDatabase(@onNull SQLiteDatabase database)36     static String pathForDatabase(@NonNull SQLiteDatabase database) {
37         return isInMemoryDatabase(database)
38                 ? String.format(sInMemoryDatabaseNameFormat, database.hashCode())
39                 : new File(database.getPath()).getAbsolutePath();
40     }
41 
42     /** Thread-safe as {@link SQLiteDatabase#getPath} is thread-safe. */
isInMemoryDatabase(@onNull SQLiteDatabase database)43     static boolean isInMemoryDatabase(@NonNull SQLiteDatabase database) {
44         return Objects.equals(sInMemoryDatabasePath, database.getPath());
45     }
46 
47     /**
48      * Attempts to call {@link SQLiteDatabase#acquireReference} on the provided object.
49      *
50      * @return true if the operation was successful; false if unsuccessful because the database
51      * was already closed; otherwise re-throws the exception thrown by
52      * {@link SQLiteDatabase#acquireReference}.
53      */
tryAcquireReference(@onNull SQLiteDatabase database)54     static boolean tryAcquireReference(@NonNull SQLiteDatabase database) {
55         if (!database.isOpen()) {
56             return false;
57         }
58 
59         try {
60             database.acquireReference();
61             return true; // success
62         } catch (IllegalStateException e) {
63             if (isAttemptAtUsingClosedDatabase(e)) {
64                 return false;
65             }
66             throw e;
67         }
68     }
69 
70     /**
71      * Note that this is best-effort as relies on Exception message parsing, which could break in
72      * the future.
73      * Use in the context where false negatives (more likely) and false positives (less likely
74      * due to the specificity of the message) are tolerable, e.g. to assign error codes where if
75      * it fails we will just send an 'unknown' error.
76      */
isAttemptAtUsingClosedDatabase(IllegalStateException exception)77     static boolean isAttemptAtUsingClosedDatabase(IllegalStateException exception) {
78         String message = exception.getMessage();
79         return message != null && (message.contains("attempt to re-open an already-closed object")
80                 || message.contains(
81                 "Cannot perform this operation because the connection pool has been closed"));
82     }
83 }
84