From 5bda0261159ec83ef60c84372b512d49d244d8c7 Mon Sep 17 00:00:00 2001 From: Jeff Gardner <202880+erskingardner@users.noreply.github.com> Date: Thu, 30 Jan 2025 18:02:25 +0100 Subject: [PATCH] Janky fix for Android database migration loading --- .../.idea/deploymentTargetSelector.xml | 2 +- src-tauri/gen/android/.idea/gradle.xml | 1 + src-tauri/gen/android/.idea/misc.xml | 1 - .../assets/db_migrations/0001_initial.sql | 169 ++++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 16 +- src-tauri/gen/android/build.gradle.kts | 2 +- .../gen/android/buildSrc/build.gradle.kts | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- src-tauri/src/database.rs | 63 ++++++- src-tauri/src/lib.rs | 2 + 10 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 src-tauri/gen/android/app/src/main/assets/db_migrations/0001_initial.sql diff --git a/src-tauri/gen/android/.idea/deploymentTargetSelector.xml b/src-tauri/gen/android/.idea/deploymentTargetSelector.xml index b268ef3..66637b7 100644 --- a/src-tauri/gen/android/.idea/deploymentTargetSelector.xml +++ b/src-tauri/gen/android/.idea/deploymentTargetSelector.xml @@ -2,7 +2,7 @@ <project version="4"> <component name="deploymentTargetSelector"> <selectionStates> - <SelectionState runConfigName="app"> + <SelectionState runConfigName="Whitenoise"> <option name="selectionMode" value="DROPDOWN" /> </SelectionState> </selectionStates> diff --git a/src-tauri/gen/android/.idea/gradle.xml b/src-tauri/gen/android/.idea/gradle.xml index c301173..1ea85cb 100644 --- a/src-tauri/gen/android/.idea/gradle.xml +++ b/src-tauri/gen/android/.idea/gradle.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> + <component name="GradleMigrationSettings" migrationVersion="1" /> <component name="GradleSettings"> <option name="linkedExternalProjectsSettings"> <GradleProjectSettings> diff --git a/src-tauri/gen/android/.idea/misc.xml b/src-tauri/gen/android/.idea/misc.xml index 74dd639..b2c751a 100644 --- a/src-tauri/gen/android/.idea/misc.xml +++ b/src-tauri/gen/android/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> diff --git a/src-tauri/gen/android/app/src/main/assets/db_migrations/0001_initial.sql b/src-tauri/gen/android/app/src/main/assets/db_migrations/0001_initial.sql new file mode 100644 index 0000000..a40396d --- /dev/null +++ b/src-tauri/gen/android/app/src/main/assets/db_migrations/0001_initial.sql @@ -0,0 +1,169 @@ +-- Accounts table with JSON fields for complex objects +CREATE TABLE accounts ( + pubkey TEXT PRIMARY KEY, + metadata TEXT NOT NULL, -- JSON string for nostr Metadata + settings TEXT NOT NULL, -- JSON string for AccountSettings + onboarding TEXT NOT NULL, -- JSON string for AccountOnboarding + last_used INTEGER NOT NULL, + last_synced INTEGER NOT NULL, + active BOOLEAN NOT NULL DEFAULT FALSE +); + +-- Create an index for faster lookups +CREATE INDEX idx_accounts_active ON accounts(active); + +-- Create a unique partial index that only allows one TRUE value +CREATE UNIQUE INDEX idx_accounts_single_active ON accounts(active) WHERE active = TRUE; + +-- Create a trigger to ensure only one active account +CREATE TRIGGER ensure_single_active_account + BEFORE UPDATE ON accounts + WHEN NEW.active = TRUE +BEGIN + UPDATE accounts SET active = FALSE WHERE active = TRUE AND pubkey != NEW.pubkey; +END; + +-- Account-specific relays table +CREATE TABLE account_relays ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT NOT NULL, + relay_type TEXT NOT NULL, + account_pubkey TEXT NOT NULL, + FOREIGN KEY (account_pubkey) REFERENCES accounts(pubkey) ON DELETE CASCADE +); + +CREATE INDEX idx_account_relays_account ON account_relays(account_pubkey, relay_type); + +-- Group-specific relays table (separate table) +CREATE TABLE group_relays ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT NOT NULL, + relay_type TEXT NOT NULL, + account_pubkey TEXT NOT NULL, + group_id BLOB NOT NULL, + FOREIGN KEY (group_id, account_pubkey) REFERENCES groups(mls_group_id, account_pubkey) ON DELETE CASCADE +); + +CREATE INDEX idx_group_relays_group ON group_relays(group_id, relay_type); +CREATE INDEX idx_group_relays_group_account ON group_relays(group_id, account_pubkey); + +-- Groups table matching the Group struct +CREATE TABLE groups ( + mls_group_id BLOB, + account_pubkey TEXT NOT NULL, + nostr_group_id TEXT NOT NULL, + name TEXT, + description TEXT, + admin_pubkeys TEXT NOT NULL, -- JSON array of strings + last_message_id TEXT, + last_message_at INTEGER, + group_type TEXT NOT NULL CHECK (group_type IN ('DirectMessage', 'Group')), + epoch INTEGER NOT NULL, + state TEXT NOT NULL CHECK (state IN ('Active', 'Inactive')), + FOREIGN KEY (account_pubkey) REFERENCES accounts(pubkey) ON DELETE CASCADE, + PRIMARY KEY (mls_group_id, account_pubkey) +); + +CREATE INDEX idx_groups_mls_group_id ON groups(mls_group_id); +CREATE INDEX idx_groups_account ON groups(account_pubkey); +CREATE INDEX idx_groups_nostr ON groups(nostr_group_id); +CREATE INDEX idx_groups_mls_group_id_account ON groups(mls_group_id, account_pubkey); + +-- Invites table matching the Invite struct +CREATE TABLE invites ( + event_id TEXT PRIMARY KEY, -- the event_id of the 444 unsigned invite event + account_pubkey TEXT NOT NULL, + event TEXT NOT NULL, -- JSON string for UnsignedEvent + mls_group_id BLOB NOT NULL, + nostr_group_id TEXT NOT NULL, + group_name TEXT NOT NULL, + group_description TEXT NOT NULL, + group_admin_pubkeys TEXT NOT NULL, -- JSON array of strings + group_relays TEXT NOT NULL, -- JSON array of strings + inviter TEXT NOT NULL, + member_count INTEGER NOT NULL, + state TEXT NOT NULL, + outer_event_id TEXT, -- the event_id of the 1059 event that contained the invite + + FOREIGN KEY (account_pubkey) REFERENCES accounts(pubkey) ON DELETE CASCADE +); + +CREATE INDEX idx_invites_mls_group ON invites(mls_group_id); +CREATE INDEX idx_invites_state ON invites(state); +CREATE INDEX idx_invites_account ON invites(account_pubkey); +CREATE INDEX idx_invites_outer_event_id ON invites(outer_event_id); +CREATE INDEX idx_invites_event_id ON invites(event_id); + +CREATE TABLE processed_invites ( + event_id TEXT PRIMARY KEY, -- This is the outer event id of the 1059 gift wrap event + invite_event_id TEXT, -- This is the event id of the 444 invite event + account_pubkey TEXT NOT NULL, -- This is the pubkey of the account that processed the invite + processed_at INTEGER NOT NULL, -- This is the timestamp of when the invite was processed + state TEXT NOT NULL, -- This is the state of the invite processing + failure_reason TEXT, -- This is the reason the invite failed to process + + FOREIGN KEY (account_pubkey) REFERENCES accounts(pubkey) ON DELETE CASCADE +); + +CREATE INDEX idx_processed_invites_invite_event_id ON processed_invites(invite_event_id); + +-- Messages table with full-text search +CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_id TEXT NOT NULL, + mls_group_id BLOB NOT NULL, + account_pubkey TEXT NOT NULL, + author_pubkey TEXT NOT NULL, + created_at INTEGER NOT NULL, + content TEXT NOT NULL, + tags TEXT, -- JSON array of nostr tags + event TEXT NOT NULL, -- JSON string for UnsignedEvent + outer_event_id TEXT NOT NULL, -- the event_id of the 445 event + FOREIGN KEY (mls_group_id, account_pubkey) REFERENCES groups(mls_group_id, account_pubkey) ON DELETE CASCADE, + FOREIGN KEY (account_pubkey) REFERENCES accounts(pubkey) ON DELETE CASCADE, + UNIQUE(event_id, account_pubkey) -- Ensure event_id is unique per account +); + +CREATE INDEX idx_messages_group_time ON messages(mls_group_id, created_at); +CREATE INDEX idx_messages_account_time ON messages(account_pubkey, created_at); +CREATE INDEX idx_messages_author_time ON messages(author_pubkey, created_at); +CREATE INDEX idx_messages_outer_event_id ON messages(outer_event_id); +CREATE INDEX idx_messages_event_id ON messages(event_id); +CREATE INDEX idx_messages_event_id_account ON messages(event_id, account_pubkey); + +-- Update processed_messages table to reference the new messages table structure +CREATE TABLE processed_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_id TEXT NOT NULL, -- This is the outer event id of the 445 event + message_event_id TEXT NOT NULL, -- This is the inner UnsignedEvent's id. This is the id of the events stored in the messages table. + account_pubkey TEXT NOT NULL, -- This is the pubkey of the account that processed the message + processed_at INTEGER NOT NULL, -- This is the timestamp of when the message was processed + state TEXT NOT NULL, -- This is the state of the message processing + failure_reason TEXT, -- This is the reason the message failed to process + UNIQUE(event_id, account_pubkey), + FOREIGN KEY (account_pubkey) REFERENCES accounts(pubkey) ON DELETE CASCADE +); + +CREATE INDEX idx_processed_messages_message_event_id ON processed_messages(message_event_id); +CREATE INDEX idx_processed_messages_event_id_account ON processed_messages(event_id, account_pubkey); + +-- Full-text search for messages +CREATE VIRTUAL TABLE messages_fts USING fts5( + content, + id UNINDEXED, -- Change to reference the new id column + content='messages' +); + +-- Update FTS triggers to use id instead of event_id +CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN + INSERT INTO messages_fts(content, id) VALUES (new.content, new.id); +END; + +CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN + INSERT INTO messages_fts(messages_fts, content, id) VALUES('delete', old.content, old.id); +END; + +CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN + INSERT INTO messages_fts(messages_fts, content, id) VALUES('delete', old.content, old.id); + INSERT INTO messages_fts(content, id) VALUES (new.content, new.id); +END; diff --git a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index d1410d2..8e33b25 100644 --- a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -3,8 +3,14 @@ android:viewportHeight="1080" android:width="1080dp" android:height="1080dp"> - <path - android:pathData="M241 770V310H395V450.901L463 310H617V450.901L685 310H839V770H685V629.099L617 770H463V629.099L395 770H241ZM371 334H265V720.27L371 500.631V334ZM709 746H815V359.73L709 579.369V746ZM601.934 746L800.769 334H700.066L501.231 746H601.934ZM578.769 334H478.066L279.231 746H379.934L578.769 334ZM593 359.73L487 579.369V720.27L593 500.631V359.73Z" - android:fillType="evenOdd" - android:fillColor="#1F1F1F" /> -</vector> \ No newline at end of file + <group + android:scaleX="0.8" + android:scaleY="0.8" + android:translateX="108" + android:translateY="108"> + <path + android:pathData="M241 770V310H395V450.901L463 310H617V450.901L685 310H839V770H685V629.099L617 770H463V629.099L395 770H241ZM371 334H265V720.27L371 500.631V334ZM709 746H815V359.73L709 579.369V746ZM601.934 746L800.769 334H700.066L501.231 746H601.934ZM578.769 334H478.066L279.231 746H379.934L578.769 334ZM593 359.73L487 579.369V720.27L593 500.631V359.73Z" + android:fillType="evenOdd" + android:fillColor="#1F1F1F" /> + </group> +</vector> diff --git a/src-tauri/gen/android/build.gradle.kts b/src-tauri/gen/android/build.gradle.kts index c5ef452..dcded35 100644 --- a/src-tauri/gen/android/build.gradle.kts +++ b/src-tauri/gen/android/build.gradle.kts @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.5.1") + classpath("com.android.tools.build:gradle:8.8.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") } } diff --git a/src-tauri/gen/android/buildSrc/build.gradle.kts b/src-tauri/gen/android/buildSrc/build.gradle.kts index 39e90b0..e874701 100644 --- a/src-tauri/gen/android/buildSrc/build.gradle.kts +++ b/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -18,6 +18,6 @@ repositories { dependencies { compileOnly(gradleApi()) - implementation("com.android.tools.build:gradle:8.5.1") + implementation("com.android.tools.build:gradle:8.8.0") } diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties index 0df10d5..593ee6b 100644 --- a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties +++ b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue May 10 19:22:52 CST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs index 07e06a4..09cb39e 100644 --- a/src-tauri/src/database.rs +++ b/src-tauri/src/database.rs @@ -1,10 +1,21 @@ use sqlx::sqlite::SqlitePoolOptions; use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool}; +use std::fs; use std::path::PathBuf; use std::time::Duration; use tauri::{path::BaseDirectory, AppHandle, Manager}; use thiserror::Error; +const MIGRATION_FILES: &[(&str, &[u8])] = &[ + ( + "0001_initial.sql", + include_bytes!("../db_migrations/0001_initial.sql"), + ), + // Add new migrations here in order, for example: + // ("0002_something.sql", include_bytes!("../db_migrations/0002_something.sql")), + // ("0003_another.sql", include_bytes!("../db_migrations/0003_another.sql")), +]; + #[derive(Error, Debug)] pub enum DatabaseError { #[error("File system error: {0}")] @@ -82,14 +93,52 @@ impl Database { // Run migrations tracing::debug!("Running migrations..."); - let migrations_path = app_handle - .path() - .resolve("db_migrations", BaseDirectory::Resource)?; - sqlx::migrate::Migrator::new(migrations_path) - .await? - .run(&pool) - .await?; + let migrations_path = if cfg!(target_os = "android") { + // On Android, we need to copy migrations to a temporary directory + let temp_dir = app_handle.path().app_data_dir()?.join("temp_migrations"); + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir)?; + } + fs::create_dir_all(&temp_dir)?; + + // Copy all migration files from the embedded assets + for (filename, content) in MIGRATION_FILES { + tracing::debug!("Writing migration file: {}", filename); + fs::write(temp_dir.join(filename), content)?; + } + + temp_dir + } else { + app_handle + .path() + .resolve("db_migrations", BaseDirectory::Resource)? + }; + + tracing::debug!("Migrations path: {:?}", migrations_path); + if !migrations_path.exists() { + tracing::error!("Migrations directory not found at {:?}", migrations_path); + return Err(DatabaseError::FileSystem(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Migrations directory not found at {:?}", migrations_path), + ))); + } + + match sqlx::migrate::Migrator::new(migrations_path).await { + Ok(migrator) => { + migrator.run(&pool).await?; + // Clean up temp directory on Android after successful migration + if cfg!(target_os = "android") { + if let Ok(temp_dir) = app_handle.path().app_data_dir() { + let _ = fs::remove_dir_all(temp_dir.join("temp_migrations")); + } + } + } + Err(e) => { + tracing::error!("Failed to create migrator: {:?}", e); + return Err(DatabaseError::Migrate(e)); + } + } Ok(Self { pool, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a7a5a19..8a987a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -43,12 +43,14 @@ pub fn run() { } else { PathBuf::from(format!("{}/release", data_dir.to_string_lossy())) }; + std::fs::create_dir_all(&formatted_data_dir)?; let formatted_logs_dir = if cfg!(dev) { PathBuf::from(format!("{}/dev", logs_dir.to_string_lossy())) } else { PathBuf::from(format!("{}/release", logs_dir.to_string_lossy())) }; + std::fs::create_dir_all(&formatted_logs_dir)?; setup_logging(formatted_logs_dir.clone())?;