FreeRDP
Loading...
Searching...
No Matches
AppDatabase.java
1/*
2 Room database for bookmark storage
3
4 Copyright 2026 Ibrahim Sevinc <ibrahim.sevinc.mail@gmail.com>
5
6 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7 If a copy of the MPL was not distributed with this file, You can obtain one at
8 http://mozilla.org/MPL/2.0/.
9*/
10
11package com.freerdp.freerdpcore.data;
12
13import android.content.Context;
14
15import androidx.annotation.NonNull;
16
17import java.io.File;
18import java.util.HashMap;
19import java.util.Map;
20
21import androidx.room.Database;
22import androidx.room.Room;
23import androidx.room.RoomDatabase;
24import androidx.room.migration.Migration;
25import androidx.sqlite.db.SupportSQLiteDatabase;
26
27import com.freerdp.freerdpcore.security.KeystoreHelper;
28
29import net.zetetic.database.sqlcipher.SQLiteDatabase;
30import net.zetetic.database.sqlcipher.SupportOpenHelperFactory;
31
32@Database(entities = { BookmarkEntity.class }, version = AppDatabase.DB_VERSION,
33 exportSchema = false)
34public abstract class AppDatabase extends RoomDatabase
35{
36 static final int DB_VERSION = 16;
37 private static final String DB_NAME = "bookmarks.db";
38
39 static
40 {
41 System.loadLibrary("sqlcipher");
42 }
43
44 private static volatile AppDatabase instance;
45
46 public abstract BookmarkDao bookmarkDao();
47
48 public static AppDatabase getInstance(Context context)
49 {
50 if (instance == null)
51 {
52 synchronized (AppDatabase.class)
53 {
54 if (instance == null)
55 {
56 byte[] key = getOrCreateDbKey(context);
57 migrateUnencryptedIfNeeded(context, key);
58
59 instance = Room.databaseBuilder(context.getApplicationContext(),
60 AppDatabase.class, DB_NAME)
61 .openHelperFactory(new SupportOpenHelperFactory(key))
62 .addMigrations(MIGRATION_10_11)
63 .addMigrations(MIGRATION_11_12)
64 .addMigrations(MIGRATION_12_13)
65 .addMigrations(MIGRATION_13_14)
66 .addMigrations(MIGRATION_14_15)
67 .addMigrations(MIGRATION_15_16)
68 .build();
69 }
70 }
71 }
72 return instance;
73 }
74
75 private static byte[] getOrCreateDbKey(Context context)
76 {
77 try
78 {
79 return KeystoreHelper.getInstance(context).getOrCreateDbKey();
80 }
81 catch (KeystoreHelper.KeystoreException e)
82 {
83 throw new RuntimeException("Cannot obtain database encryption key", e);
84 }
85 }
86
87 // Converts an existing unencrypted database to SQLCipher in-place
88 private static void migrateUnencryptedIfNeeded(Context context, byte[] key)
89 {
90 File dbFile = context.getDatabasePath(DB_NAME);
91 if (!dbFile.exists())
92 return;
93
94 String path = dbFile.getAbsolutePath();
95
96 // Check if already encrypted by trying to open with the key.
97 try
98 {
99 SQLiteDatabase.openDatabase(path, key, null, SQLiteDatabase.OPEN_READONLY, null, null)
100 .close();
101 return; // Database is already encrypted, no migration needed
102 }
103 catch (Exception ignored)
104 {
105 }
106
107 SQLiteDatabase db = null;
108 try
109 {
110 // Verify it is an unencrypted database by opening it with an empty key
111 db = SQLiteDatabase.openDatabase(path, new byte[0], null, SQLiteDatabase.OPEN_READWRITE,
112 null, null);
113 }
114 catch (Exception e)
115 {
116 // If it fails, the file might be corrupted (not plaintext, not correctly encrypted)
117 SQLiteDatabase.deleteDatabase(dbFile);
118 return;
119 }
120
121 // https://www.zetetic.net/sqlcipher/sqlcipher-api/index.html#sqlcipher_export
122 // Prepare a temporary file for the encrypted database migration
123 String tmpPath = path + ".migrating";
124 File tmpFile = new File(tmpPath);
125 SQLiteDatabase.deleteDatabase(tmpFile);
126
127 try
128 {
129 // Pre-create the encrypted database to avoid CANTOPEN errors on ATTACH
130 SQLiteDatabase
131 .openDatabase(tmpPath, key, null,
132 SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY,
133 null, null)
134 .close();
135 }
136 catch (Exception ignored)
137 {
138 }
139
140 try
141 {
142 try
143 {
144 db.execSQL("ATTACH DATABASE '" + tmpPath + "' AS encrypted KEY X'" + toHex(key) +
145 "'");
146 db.rawQuery("SELECT sqlcipher_export('encrypted')", null).moveToFirst();
147 db.execSQL("DETACH DATABASE encrypted");
148 }
149 finally
150 {
151 db.close();
152 }
153
154 // Replace the old unencrypted database with the new encrypted one
155 SQLiteDatabase.deleteDatabase(dbFile);
156 if (!tmpFile.renameTo(dbFile))
157 throw new RuntimeException("Could not replace database file after encryption");
158 }
159 catch (Exception e)
160 {
161 SQLiteDatabase.deleteDatabase(tmpFile);
162 throw new RuntimeException("Failed to encrypt existing database", e);
163 }
164 }
165
166 private static String toHex(byte[] bytes)
167 {
168 StringBuilder sb = new StringBuilder(bytes.length * 2);
169 for (byte b : bytes)
170 sb.append(String.format(java.util.Locale.US, "%02x", b));
171 return sb.toString();
172 }
173
174 private static final Migration MIGRATION_15_16 = new Migration(15, 16) {
175 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
176 {
177 db.execSQL(
178 "ALTER TABLE 'bookmarks' ADD 'redirect_printer' INTEGER NOT NULL DEFAULT false;");
179 }
180 };
181
182 private static final Migration MIGRATION_14_15 = new Migration(14, 15) {
183 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
184 {
185 db.execSQL("ALTER TABLE 'bookmarks' ADD 'scale_mode' TEXT NOT NULL DEFAULT '100';");
186 db.execSQL("ALTER TABLE 'bookmarks' ADD 'scale_desktop' INTEGER NOT NULL DEFAULT 100;");
187 db.execSQL("ALTER TABLE 'bookmarks' ADD 'scale_device' INTEGER NOT NULL DEFAULT 100;");
188 db.execSQL(
189 "ALTER TABLE 'bookmarks' ADD 'vmconnect_mode' INTEGER NOT NULL DEFAULT false;");
190 db.execSQL("ALTER TABLE 'bookmarks' ADD 'vmconnect_guid' TEXT NOT NULL DEFAULT '';");
191 }
192 };
193
194 private static final Migration MIGRATION_13_14 = new Migration(13, 14) {
195 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
196 {
197 final String[] columns = new String[] {
198 "label", "hostname", "username", "password",
199 "domain", "gateway_hostname", "gateway_username", "gateway_password",
200 "gateway_domain", "remote_program", "work_dir", "loadbalanceinfo"
201 };
202 for (String column : columns)
203 {
204 db.execSQL("ALTER TABLE 'bookmarks' ADD '" + column +
205 "_with_default' TEXT NOT NULL DEFAULT '';");
206 db.execSQL("UPDATE bookmarks SET " + column + "_with_default = " + column);
207 db.execSQL("ALTER TABLE 'bookmarks' DROP '" + column + "';");
208 db.execSQL("ALTER TABLE 'bookmarks' RENAME COLUMN '" + column +
209 "_with_default' to '" + column + "';");
210 }
211
212 final String[] debugColumns = new String[] { "debug_level" };
213 for (String column : debugColumns)
214 {
215 db.execSQL("ALTER TABLE 'bookmarks' ADD '" + column +
216 "_with_default' TEXT NOT NULL DEFAULT 'INFO';");
217 db.execSQL("UPDATE bookmarks SET " + column + "_with_default = " + column);
218 db.execSQL("ALTER TABLE 'bookmarks' DROP '" + column + "';");
219 db.execSQL("ALTER TABLE 'bookmarks' RENAME COLUMN '" + column +
220 "_with_default' to '" + column + "';");
221 }
222 final Map<String, Integer> intColumns = new HashMap<>();
223 intColumns.put("port", 3389);
224 intColumns.put("colors", 32);
225 intColumns.put("resolution", -1);
226 intColumns.put("width", 0);
227 intColumns.put("height", 0);
228 intColumns.put("gateway_port", 443);
229 intColumns.put("redirect_sound", 0);
230 intColumns.put("security", 0);
231 intColumns.put("tlsSecLevel", -1);
232 intColumns.put("tlsMinLevel", -1);
233
234 for (Map.Entry<String, Integer> column : intColumns.entrySet())
235 {
236 db.execSQL("ALTER TABLE 'bookmarks' ADD '" + column.getKey() +
237 "_with_default' INTEGER NOT NULL DEFAULT " +
238 column.getValue().toString() + ";");
239 db.execSQL("UPDATE bookmarks SET " + column.getKey() +
240 "_with_default = " + column.getKey());
241 db.execSQL("ALTER TABLE 'bookmarks' DROP '" + column.getKey() + "';");
242 db.execSQL("ALTER TABLE 'bookmarks' RENAME COLUMN '" + column.getKey() +
243 "_with_default' to '" + column.getKey() + "';");
244 }
245
246 final Map<String, Boolean> boolColumns = new HashMap<>();
247 boolColumns.put("perf_remotefx", true);
248 boolColumns.put("perf_gfx", true);
249 boolColumns.put("perf_gfx_h264", true);
250 boolColumns.put("perf_wallpaper", true);
251 boolColumns.put("perf_theming", true);
252 boolColumns.put("perf_full_window_drag", true);
253 boolColumns.put("perf_menu_animations", true);
254 boolColumns.put("perf_font_smoothing", true);
255 boolColumns.put("perf_desktop_composition", true);
256 boolColumns.put("enable_gateway_settings", false);
257 boolColumns.put("redirect_sdcard", false);
258 boolColumns.put("redirect_microphone", false);
259 boolColumns.put("console_mode", false);
260 boolColumns.put("async_channel", false);
261 boolColumns.put("async_update", false);
262
263 for (Map.Entry<String, Boolean> column : boolColumns.entrySet())
264 {
265 db.execSQL("ALTER TABLE 'bookmarks' ADD '" + column.getKey() +
266 "_with_default' INTEGER NOT NULL DEFAULT " +
267 column.getValue().toString() + ";");
268 db.execSQL("UPDATE bookmarks SET " + column.getKey() +
269 "_with_default = " + column.getKey());
270 db.execSQL("ALTER TABLE 'bookmarks' DROP '" + column.getKey() + "';");
271 db.execSQL("ALTER TABLE 'bookmarks' RENAME COLUMN '" + column.getKey() +
272 "_with_default' to '" + column.getKey() + "';");
273 }
274 }
275 };
276
277 private static final Migration MIGRATION_12_13 = new Migration(12, 13) {
278 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
279 {
280 db.execSQL("ALTER TABLE 'bookmarks' ADD 'loadbalanceinfo' TEXT NOT NULL DEFAULT '';");
281 }
282 };
283
284 private static final Migration MIGRATION_11_12 = new Migration(11, 12) {
285 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
286 {
287 db.execSQL("ALTER TABLE 'bookmarks' ADD 'tlsSecLevel' INTEGER NOT NULL CONSTRAINT "
288 + "chk_tlsSecLevel "
289 + "CHECK (tlsSecLevel >= -1 AND tlsSecLevel <= 5) DEFAULT -1;");
290 db.execSQL("ALTER TABLE 'bookmarks' ADD 'tlsMinLevel' INTEGER NOT NULL CONSTRAINT "
291 + "chk_tlsMinLevel "
292 + "CHECK (tlsMinLevel >= -1) DEFAULT -1;");
293 final String[] list = { "screen_3g_colors",
294 "screen_3g_resolution",
295 "screen_3g_width",
296 "screen_3g_height",
297 "perf_3g_remotefx",
298 "perf_3g_gfx",
299 "perf_3g_gfx_h264",
300 "perf_3g_wallpaper",
301 "perf_3g_theming",
302 "perf_3g_full_window_drag",
303 "perf_3g_menu_animations",
304 "perf_3g_font_smoothing",
305 "perf_3g_desktop_composition",
306 "enable_3g_settings" };
307
308 for (String s : list)
309 {
310 db.execSQL("ALTER TABLE 'bookmarks' DROP COLUMN '" + s + "';");
311 }
312 }
313 };
314
315 // v10: tbl_manual_bookmarks + tbl_screen_settings + tbl_performance_flags (SQLiteOpenHelper)
316 // v11: single flat `bookmarks` table (Room)
317 private static final Migration MIGRATION_10_11 = new Migration(10, 11) {
318 @Override public void migrate(@NonNull SupportSQLiteDatabase db)
319 {
320 db.execSQL("CREATE TABLE IF NOT EXISTS `bookmarks` ("
321 + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
322 + "`label` TEXT,"
323 + "`hostname` TEXT,"
324 + "`username` TEXT,"
325 + "`password` TEXT,"
326 + "`domain` TEXT,"
327 + "`port` INTEGER NOT NULL,"
328 + "`colors` INTEGER NOT NULL,"
329 + "`resolution` INTEGER NOT NULL,"
330 + "`width` INTEGER NOT NULL,"
331 + "`height` INTEGER NOT NULL,"
332 + "`perf_remotefx` INTEGER NOT NULL,"
333 + "`perf_gfx` INTEGER NOT NULL,"
334 + "`perf_gfx_h264` INTEGER NOT NULL,"
335 + "`perf_wallpaper` INTEGER NOT NULL,"
336 + "`perf_theming` INTEGER NOT NULL,"
337 + "`perf_full_window_drag` INTEGER NOT NULL,"
338 + "`perf_menu_animations` INTEGER NOT NULL,"
339 + "`perf_font_smoothing` INTEGER NOT NULL,"
340 + "`perf_desktop_composition` INTEGER NOT NULL,"
341 + "`screen_3g_colors` INTEGER NOT NULL,"
342 + "`screen_3g_resolution` INTEGER NOT NULL,"
343 + "`screen_3g_width` INTEGER NOT NULL,"
344 + "`screen_3g_height` INTEGER NOT NULL,"
345 + "`perf_3g_remotefx` INTEGER NOT NULL,"
346 + "`perf_3g_gfx` INTEGER NOT NULL,"
347 + "`perf_3g_gfx_h264` INTEGER NOT NULL,"
348 + "`perf_3g_wallpaper` INTEGER NOT NULL,"
349 + "`perf_3g_theming` INTEGER NOT NULL,"
350 + "`perf_3g_full_window_drag` INTEGER NOT NULL,"
351 + "`perf_3g_menu_animations` INTEGER NOT NULL,"
352 + "`perf_3g_font_smoothing` INTEGER NOT NULL,"
353 + "`perf_3g_desktop_composition` INTEGER NOT NULL,"
354 + "`enable_3g_settings` INTEGER NOT NULL,"
355 + "`enable_gateway_settings` INTEGER NOT NULL,"
356 + "`gateway_hostname` TEXT,"
357 + "`gateway_port` INTEGER NOT NULL,"
358 + "`gateway_username` TEXT,"
359 + "`gateway_password` TEXT,"
360 + "`gateway_domain` TEXT,"
361 + "`redirect_sdcard` INTEGER NOT NULL,"
362 + "`redirect_sound` INTEGER NOT NULL,"
363 + "`redirect_microphone` INTEGER NOT NULL,"
364 + "`security` INTEGER NOT NULL,"
365 + "`remote_program` TEXT,"
366 + "`work_dir` TEXT,"
367 + "`console_mode` INTEGER NOT NULL,"
368 + "`debug_level` TEXT,"
369 + "`async_channel` INTEGER NOT NULL,"
370 + "`async_update` INTEGER NOT NULL"
371 + ")");
372
373 // port was stored as TEXT in the old schema
374 db.execSQL(
375 "INSERT INTO bookmarks ("
376 + " id, label, hostname, username, password, domain, port,"
377 + " colors, resolution, width, height,"
378 + " perf_remotefx, perf_gfx, perf_gfx_h264, perf_wallpaper, perf_theming,"
379 + " perf_full_window_drag, perf_menu_animations, perf_font_smoothing, "
380 + "perf_desktop_composition,"
381 + " screen_3g_colors, screen_3g_resolution, screen_3g_width, screen_3g_height,"
382 + " perf_3g_remotefx, perf_3g_gfx, perf_3g_gfx_h264, perf_3g_wallpaper, "
383 + "perf_3g_theming,"
384 + " perf_3g_full_window_drag, perf_3g_menu_animations, perf_3g_font_smoothing, "
385 + "perf_3g_desktop_composition,"
386 + " enable_3g_settings, enable_gateway_settings,"
387 + " gateway_hostname, gateway_port, gateway_username, gateway_password, "
388 + "gateway_domain,"
389 + " redirect_sdcard, redirect_sound, redirect_microphone,"
390 + " security, remote_program, work_dir, console_mode,"
391 + " debug_level, async_channel, async_update"
392 + ") SELECT"
393 + " b._id, IFNULL(b.label, ''), IFNULL(b.hostname, ''), IFNULL(b.username, ''), "
394 + "b.password, b.domain,"
395 + " IFNULL(CAST(NULLIF(b.port, '') AS INTEGER), 3389),"
396 + " IFNULL(s.colors, 32), IFNULL(s.resolution, -1), IFNULL(s.width, 0), "
397 + "IFNULL(s.height, 0),"
398 + " IFNULL(p.perf_remotefx, 0), IFNULL(p.perf_gfx, 1), IFNULL(p.perf_gfx_h264, "
399 + "0), IFNULL(p.perf_wallpaper, 0), IFNULL(p.perf_theming, 0),"
400 + " IFNULL(p.perf_full_window_drag, 0), IFNULL(p.perf_menu_animations, 0), "
401 + "IFNULL(p.perf_font_smoothing, 0), IFNULL(p.perf_desktop_composition, 0),"
402 + " IFNULL(s3.colors, 16), IFNULL(s3.resolution, -1), IFNULL(s3.width, 0), "
403 + "IFNULL(s3.height, 0),"
404 + " IFNULL(p3.perf_remotefx, 0), IFNULL(p3.perf_gfx, 0), IFNULL(p3.perf_gfx_h264, "
405 + "0), IFNULL(p3.perf_wallpaper, 0), IFNULL(p3.perf_theming, 0),"
406 + " IFNULL(p3.perf_full_window_drag, 0), IFNULL(p3.perf_menu_animations, 0), "
407 + "IFNULL(p3.perf_font_smoothing, 0), IFNULL(p3.perf_desktop_composition, 0),"
408 + " IFNULL(b.enable_3g_settings, 0), IFNULL(b.enable_gateway_settings, 0),"
409 + " b.gateway_hostname, IFNULL(b.gateway_port, 443), b.gateway_username, "
410 + "b.gateway_password, b.gateway_domain,"
411 + " IFNULL(b.redirect_sdcard, 0), IFNULL(b.redirect_sound, 0), "
412 + "IFNULL(b.redirect_microphone, 0),"
413 +
414 " IFNULL(b.security, 0), b.remote_program, b.work_dir, IFNULL(b.console_mode, 0),"
415 + " IFNULL(b.debug_level, 'INFO'), IFNULL(b.async_channel, 0), "
416 + "IFNULL(b.async_update, 0)"
417 + " FROM tbl_manual_bookmarks b"
418 + " LEFT JOIN tbl_screen_settings s ON s._id = b.screen_settings"
419 + " LEFT JOIN tbl_screen_settings s3 ON s3._id = b.screen_3g"
420 + " LEFT JOIN tbl_performance_flags p ON p._id = b.performance_flags"
421 + " LEFT JOIN tbl_performance_flags p3 ON p3._id = b.performance_3g");
422
423 db.execSQL("DROP TABLE IF EXISTS tbl_manual_bookmarks");
424 db.execSQL("DROP TABLE IF EXISTS tbl_screen_settings");
425 db.execSQL("DROP TABLE IF EXISTS tbl_performance_flags");
426 }
427 };
428}