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