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())?;