1 /*
2  * Copyright 2019 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 com.android.bluetooth.btservice.storage;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.database.SQLException;
22 
23 import androidx.room.Database;
24 import androidx.room.Room;
25 import androidx.room.RoomDatabase;
26 import androidx.room.migration.Migration;
27 import androidx.sqlite.db.SupportSQLiteDatabase;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.util.List;
32 
33 /**
34  * MetadataDatabase is a Room database stores Bluetooth persistence data
35  */
36 @Database(entities = {Metadata.class}, version = 104)
37 public abstract class MetadataDatabase extends RoomDatabase {
38     /**
39      * The metadata database file name
40      */
41     public static final String DATABASE_NAME = "bluetooth_db";
42 
43     static int sCurrentConnectionNumber = 0;
44 
mMetadataDao()45     protected abstract MetadataDao mMetadataDao();
46 
47     /**
48      * Create a {@link MetadataDatabase} database with migrations
49      *
50      * @param context the Context to create database
51      * @return the created {@link MetadataDatabase}
52      */
createDatabase(Context context)53     public static MetadataDatabase createDatabase(Context context) {
54         return Room.databaseBuilder(context,
55                 MetadataDatabase.class, DATABASE_NAME)
56                 .addMigrations(MIGRATION_100_101)
57                 .addMigrations(MIGRATION_101_102)
58                 .addMigrations(MIGRATION_102_103)
59                 .addMigrations(MIGRATION_103_104)
60                 .allowMainThreadQueries()
61                 .build();
62     }
63 
64     /**
65      * Create a {@link MetadataDatabase} database without migration, database
66      * would be reset if any load failure happens
67      *
68      * @param context the Context to create database
69      * @return the created {@link MetadataDatabase}
70      */
createDatabaseWithoutMigration(Context context)71     public static MetadataDatabase createDatabaseWithoutMigration(Context context) {
72         return Room.databaseBuilder(context,
73                 MetadataDatabase.class, DATABASE_NAME)
74                 .fallbackToDestructiveMigration()
75                 .allowMainThreadQueries()
76                 .build();
77     }
78 
79     /**
80      * Insert a {@link Metadata} to metadata table
81      *
82      * @param metadata the data wish to put into storage
83      */
insert(Metadata... metadata)84     public void insert(Metadata... metadata) {
85         mMetadataDao().insert(metadata);
86     }
87 
88     /**
89      * Load all data from metadata table as a {@link List} of {@link Metadata}
90      *
91      * @return a {@link List} of {@link Metadata}
92      */
load()93     public List<Metadata> load() {
94         return mMetadataDao().load();
95     }
96 
97     /**
98      * Delete one of the {@link Metadata} contained in the metadata table
99      *
100      * @param address the address of Metadata to delete
101      */
delete(String address)102     public void delete(String address) {
103         mMetadataDao().delete(address);
104     }
105 
106     /**
107      * Clear metadata table.
108      */
deleteAll()109     public void deleteAll() {
110         mMetadataDao().deleteAll();
111     }
112 
113     @VisibleForTesting
114     static final Migration MIGRATION_100_101 = new Migration(100, 101) {
115         @Override
116         public void migrate(SupportSQLiteDatabase database) {
117             database.execSQL("ALTER TABLE metadata ADD COLUMN `pbap_client_priority` INTEGER");
118         }
119     };
120 
121     @VisibleForTesting
122     static final Migration MIGRATION_101_102 = new Migration(101, 102) {
123         @Override
124         public void migrate(SupportSQLiteDatabase database) {
125             database.execSQL("CREATE TABLE IF NOT EXISTS `metadata_tmp` ("
126                     + "`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, "
127                     + "`a2dpSupportsOptionalCodecs` INTEGER NOT NULL, "
128                     + "`a2dpOptionalCodecsEnabled` INTEGER NOT NULL, "
129                     + "`a2dp_priority` INTEGER, `a2dp_sink_priority` INTEGER, "
130                     + "`hfp_priority` INTEGER, `hfp_client_priority` INTEGER, "
131                     + "`hid_host_priority` INTEGER, `pan_priority` INTEGER, "
132                     + "`pbap_priority` INTEGER, `pbap_client_priority` INTEGER, "
133                     + "`map_priority` INTEGER, `sap_priority` INTEGER, "
134                     + "`hearing_aid_priority` INTEGER, `map_client_priority` INTEGER, "
135                     + "`manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, "
136                     + "`hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, "
137                     + "`is_untethered_headset` BLOB, `untethered_left_icon` BLOB, "
138                     + "`untethered_right_icon` BLOB, `untethered_case_icon` BLOB, "
139                     + "`untethered_left_battery` BLOB, `untethered_right_battery` BLOB, "
140                     + "`untethered_case_battery` BLOB, `untethered_left_charging` BLOB, "
141                     + "`untethered_right_charging` BLOB, `untethered_case_charging` BLOB, "
142                     + "`enhanced_settings_ui_uri` BLOB, PRIMARY KEY(`address`))");
143 
144             database.execSQL("INSERT INTO metadata_tmp ("
145                     + "address, migrated, a2dpSupportsOptionalCodecs, a2dpOptionalCodecsEnabled, "
146                     + "a2dp_priority, a2dp_sink_priority, hfp_priority, hfp_client_priority, "
147                     + "hid_host_priority, pan_priority, pbap_priority, pbap_client_priority, "
148                     + "map_priority, sap_priority, hearing_aid_priority, map_client_priority, "
149                     + "manufacturer_name, model_name, software_version, hardware_version, "
150                     + "companion_app, main_icon, is_untethered_headset, untethered_left_icon, "
151                     + "untethered_right_icon, untethered_case_icon, untethered_left_battery, "
152                     + "untethered_right_battery, untethered_case_battery, "
153                     + "untethered_left_charging, untethered_right_charging, "
154                     + "untethered_case_charging, enhanced_settings_ui_uri) "
155                     + "SELECT "
156                     + "address, migrated, a2dpSupportsOptionalCodecs, a2dpOptionalCodecsEnabled, "
157                     + "a2dp_priority, a2dp_sink_priority, hfp_priority, hfp_client_priority, "
158                     + "hid_host_priority, pan_priority, pbap_priority, pbap_client_priority, "
159                     + "map_priority, sap_priority, hearing_aid_priority, map_client_priority, "
160                     + "CAST (manufacturer_name AS BLOB), "
161                     + "CAST (model_name AS BLOB), "
162                     + "CAST (software_version AS BLOB), "
163                     + "CAST (hardware_version AS BLOB), "
164                     + "CAST (companion_app AS BLOB), "
165                     + "CAST (main_icon AS BLOB), "
166                     + "CAST (is_unthethered_headset AS BLOB), "
167                     + "CAST (unthethered_left_icon AS BLOB), "
168                     + "CAST (unthethered_right_icon AS BLOB), "
169                     + "CAST (unthethered_case_icon AS BLOB), "
170                     + "CAST (unthethered_left_battery AS BLOB), "
171                     + "CAST (unthethered_right_battery AS BLOB), "
172                     + "CAST (unthethered_case_battery AS BLOB), "
173                     + "CAST (unthethered_left_charging AS BLOB), "
174                     + "CAST (unthethered_right_charging AS BLOB), "
175                     + "CAST (unthethered_case_charging AS BLOB), "
176                     + "CAST (enhanced_settings_ui_uri AS BLOB)"
177                     + "FROM metadata");
178 
179             database.execSQL("DROP TABLE `metadata`");
180             database.execSQL("ALTER TABLE `metadata_tmp` RENAME TO `metadata`");
181         }
182     };
183 
184     @VisibleForTesting
185     static final Migration MIGRATION_102_103 = new Migration(102, 103) {
186         @Override
187         public void migrate(SupportSQLiteDatabase database) {
188             try {
189                 database.execSQL("CREATE TABLE IF NOT EXISTS `metadata_tmp` ("
190                         + "`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, "
191                         + "`a2dpSupportsOptionalCodecs` INTEGER NOT NULL, "
192                         + "`a2dpOptionalCodecsEnabled` INTEGER NOT NULL, "
193                         + "`a2dp_connection_policy` INTEGER, "
194                         + "`a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, "
195                         + "`hfp_client_connection_policy` INTEGER, "
196                         + "`hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, "
197                         + "`pbap_connection_policy` INTEGER, "
198                         + "`pbap_client_connection_policy` INTEGER, "
199                         + "`map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, "
200                         + "`hearing_aid_connection_policy` INTEGER, "
201                         + "`map_client_connection_policy` INTEGER, `manufacturer_name` BLOB, "
202                         + "`model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, "
203                         + "`companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, "
204                         + "`untethered_left_icon` BLOB, `untethered_right_icon` BLOB, "
205                         + "`untethered_case_icon` BLOB, `untethered_left_battery` BLOB, "
206                         + "`untethered_right_battery` BLOB, `untethered_case_battery` BLOB, "
207                         + "`untethered_left_charging` BLOB, `untethered_right_charging` BLOB, "
208                         + "`untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, "
209                         + "PRIMARY KEY(`address`))");
210 
211                 database.execSQL("INSERT INTO metadata_tmp ("
212                         + "address, migrated, a2dpSupportsOptionalCodecs, "
213                         + "a2dpOptionalCodecsEnabled, a2dp_connection_policy, "
214                         + "a2dp_sink_connection_policy, hfp_connection_policy,"
215                         + "hfp_client_connection_policy, hid_host_connection_policy,"
216                         + "pan_connection_policy, pbap_connection_policy,"
217                         + "pbap_client_connection_policy, map_connection_policy, "
218                         + "sap_connection_policy, hearing_aid_connection_policy, "
219                         + "map_client_connection_policy, manufacturer_name, model_name, "
220                         + "software_version, hardware_version, companion_app, main_icon, "
221                         + "is_untethered_headset, untethered_left_icon, untethered_right_icon, "
222                         + "untethered_case_icon, untethered_left_battery, "
223                         + "untethered_right_battery, untethered_case_battery, "
224                         + "untethered_left_charging, untethered_right_charging, "
225                         + "untethered_case_charging, enhanced_settings_ui_uri) "
226                         + "SELECT "
227                         + "address, migrated, a2dpSupportsOptionalCodecs, "
228                         + "a2dpOptionalCodecsEnabled, a2dp_priority, a2dp_sink_priority, "
229                         + "hfp_priority, hfp_client_priority, hid_host_priority, pan_priority, "
230                         + "pbap_priority, pbap_client_priority, map_priority, sap_priority, "
231                         + "hearing_aid_priority, map_client_priority, "
232                         + "CAST (manufacturer_name AS BLOB), "
233                         + "CAST (model_name AS BLOB), "
234                         + "CAST (software_version AS BLOB), "
235                         + "CAST (hardware_version AS BLOB), "
236                         + "CAST (companion_app AS BLOB), "
237                         + "CAST (main_icon AS BLOB), "
238                         + "CAST (is_untethered_headset AS BLOB), "
239                         + "CAST (untethered_left_icon AS BLOB), "
240                         + "CAST (untethered_right_icon AS BLOB), "
241                         + "CAST (untethered_case_icon AS BLOB), "
242                         + "CAST (untethered_left_battery AS BLOB), "
243                         + "CAST (untethered_right_battery AS BLOB), "
244                         + "CAST (untethered_case_battery AS BLOB), "
245                         + "CAST (untethered_left_charging AS BLOB), "
246                         + "CAST (untethered_right_charging AS BLOB), "
247                         + "CAST (untethered_case_charging AS BLOB), "
248                         + "CAST (enhanced_settings_ui_uri AS BLOB)"
249                         + "FROM metadata");
250 
251                 database.execSQL("UPDATE metadata_tmp SET a2dp_connection_policy = 100 "
252                         + "WHERE a2dp_connection_policy = 1000");
253                 database.execSQL("UPDATE metadata_tmp SET a2dp_sink_connection_policy = 100 "
254                         + "WHERE a2dp_sink_connection_policy = 1000");
255                 database.execSQL("UPDATE metadata_tmp SET hfp_connection_policy = 100 "
256                         + "WHERE hfp_connection_policy = 1000");
257                 database.execSQL("UPDATE metadata_tmp SET hfp_client_connection_policy = 100 "
258                         + "WHERE hfp_client_connection_policy = 1000");
259                 database.execSQL("UPDATE metadata_tmp SET hid_host_connection_policy = 100 "
260                         + "WHERE hid_host_connection_policy = 1000");
261                 database.execSQL("UPDATE metadata_tmp SET pan_connection_policy = 100 "
262                         + "WHERE pan_connection_policy = 1000");
263                 database.execSQL("UPDATE metadata_tmp SET pbap_connection_policy = 100 "
264                         + "WHERE pbap_connection_policy = 1000");
265                 database.execSQL("UPDATE metadata_tmp SET pbap_client_connection_policy = 100 "
266                         + "WHERE pbap_client_connection_policy = 1000");
267                 database.execSQL("UPDATE metadata_tmp SET map_connection_policy = 100 "
268                         + "WHERE map_connection_policy = 1000");
269                 database.execSQL("UPDATE metadata_tmp SET sap_connection_policy = 100 "
270                         + "WHERE sap_connection_policy = 1000");
271                 database.execSQL("UPDATE metadata_tmp SET hearing_aid_connection_policy = 100 "
272                         + "WHERE hearing_aid_connection_policy = 1000");
273                 database.execSQL("UPDATE metadata_tmp SET map_client_connection_policy = 100 "
274                         + "WHERE map_client_connection_policy = 1000");
275 
276                 database.execSQL("DROP TABLE `metadata`");
277                 database.execSQL("ALTER TABLE `metadata_tmp` RENAME TO `metadata`");
278             } catch (SQLException ex) {
279                 // Check if user has new schema, but is just missing the version update
280                 Cursor cursor = database.query("SELECT * FROM metadata");
281                 if (cursor == null || cursor.getColumnIndex("a2dp_connection_policy") == -1) {
282                     throw ex;
283                 }
284             }
285         }
286     };
287 
288     @VisibleForTesting
289     static final Migration MIGRATION_103_104 = new Migration(103, 104) {
290         @Override
291         public void migrate(SupportSQLiteDatabase database) {
292             try {
293                 database.execSQL("ALTER TABLE metadata ADD COLUMN `last_active_time` "
294                         + "INTEGER NOT NULL DEFAULT -1");
295                 database.execSQL("ALTER TABLE metadata ADD COLUMN `is_active_a2dp_device` "
296                         + "INTEGER NOT NULL DEFAULT 0");
297             } catch (SQLException ex) {
298                 // Check if user has new schema, but is just missing the version update
299                 Cursor cursor = database.query("SELECT * FROM metadata");
300                 if (cursor == null || cursor.getColumnIndex("last_active_time") == -1) {
301                     throw ex;
302                 }
303             }
304         }
305     };
306 }
307