/** * DailyNotificationMigration.java * * Migration utilities for transitioning from SharedPreferences to SQLite * Handles data migration while preserving existing notification data * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; import android.util.Log; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; /** * Handles migration from SharedPreferences to SQLite database * * This class provides utilities to: * - Migrate existing notification data from SharedPreferences * - Preserve all existing notification content during transition * - Provide backward compatibility during migration period * - Validate migration success */ public class DailyNotificationMigration { private static final String TAG = "DailyNotificationMigration"; private static final String PREFS_NAME = "DailyNotificationPrefs"; private static final String KEY_NOTIFICATIONS = "notifications"; private static final String KEY_SETTINGS = "settings"; private static final String KEY_LAST_FETCH = "last_fetch"; private static final String KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling"; private final Context context; private final DailyNotificationDatabase database; private final Gson gson; /** * Constructor * * @param context Application context * @param database SQLite database instance */ public DailyNotificationMigration(Context context, DailyNotificationDatabase database) { this.context = context; this.database = database; this.gson = new Gson(); } /** * Perform complete migration from SharedPreferences to SQLite * * @return true if migration was successful */ public boolean migrateToSQLite() { try { Log.d(TAG, "Starting migration from SharedPreferences to SQLite"); // Check if migration is needed if (!isMigrationNeeded()) { Log.d(TAG, "Migration not needed - SQLite already up to date"); return true; } // Get writable database SQLiteDatabase db = database.getWritableDatabase(); // Start transaction for atomic migration db.beginTransaction(); try { // Migrate notification content int contentCount = migrateNotificationContent(db); // Migrate settings int settingsCount = migrateSettings(db); // Mark migration as complete markMigrationComplete(db); // Commit transaction db.setTransactionSuccessful(); Log.i(TAG, String.format("Migration completed successfully: %d notifications, %d settings", contentCount, settingsCount)); return true; } catch (Exception e) { Log.e(TAG, "Error during migration transaction", e); db.endTransaction(); return false; } finally { db.endTransaction(); } } catch (Exception e) { Log.e(TAG, "Error during migration", e); return false; } } /** * Check if migration is needed * * @return true if migration is required */ private boolean isMigrationNeeded() { try { // Check if SharedPreferences has data SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]"); // Check if SQLite already has data SQLiteDatabase db = database.getReadableDatabase(); android.database.Cursor cursor = db.rawQuery( "SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null); int sqliteCount = 0; if (cursor.moveToFirst()) { sqliteCount = cursor.getInt(0); } cursor.close(); // Migration needed if SharedPreferences has data but SQLite doesn't boolean hasPrefsData = !notificationsJson.equals("[]") && !notificationsJson.isEmpty(); boolean needsMigration = hasPrefsData && sqliteCount == 0; Log.d(TAG, String.format("Migration check: prefs_data=%s, sqlite_count=%d, needed=%s", hasPrefsData, sqliteCount, needsMigration)); return needsMigration; } catch (Exception e) { Log.e(TAG, "Error checking migration status", e); return false; } } /** * Migrate notification content from SharedPreferences to SQLite * * @param db SQLite database instance * @return Number of notifications migrated */ private int migrateNotificationContent(SQLiteDatabase db) { try { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]"); if (notificationsJson.equals("[]") || notificationsJson.isEmpty()) { Log.d(TAG, "No notification content to migrate"); return 0; } // Parse JSON to List Type type = new TypeToken>(){}.getType(); List notifications = gson.fromJson(notificationsJson, type); int migratedCount = 0; for (NotificationContent notification : notifications) { try { // Create ContentValues for notif_contents table ContentValues values = new ContentValues(); values.put(DailyNotificationDatabase.COL_CONTENTS_SLOT_ID, notification.getId()); values.put(DailyNotificationDatabase.COL_CONTENTS_PAYLOAD_JSON, gson.toJson(notification)); values.put(DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT, notification.getFetchedAt()); // ETag is null for migrated data values.putNull(DailyNotificationDatabase.COL_CONTENTS_ETAG); // Insert into notif_contents table long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null, values); if (rowId != -1) { migratedCount++; Log.d(TAG, "Migrated notification: " + notification.getId()); } else { Log.w(TAG, "Failed to migrate notification: " + notification.getId()); } } catch (Exception e) { Log.e(TAG, "Error migrating notification: " + notification.getId(), e); } } Log.i(TAG, "Migrated " + migratedCount + " notifications to SQLite"); return migratedCount; } catch (Exception e) { Log.e(TAG, "Error migrating notification content", e); return 0; } } /** * Migrate settings from SharedPreferences to SQLite * * @param db SQLite database instance * @return Number of settings migrated */ private int migrateSettings(SQLiteDatabase db) { try { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); int migratedCount = 0; // Migrate last_fetch timestamp long lastFetch = prefs.getLong(KEY_LAST_FETCH, 0); if (lastFetch > 0) { ContentValues values = new ContentValues(); values.put(DailyNotificationDatabase.COL_CONFIG_K, KEY_LAST_FETCH); values.put(DailyNotificationDatabase.COL_CONFIG_V, String.valueOf(lastFetch)); long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values); if (rowId != -1) { migratedCount++; Log.d(TAG, "Migrated last_fetch setting"); } } // Migrate adaptive_scheduling setting boolean adaptiveScheduling = prefs.getBoolean(KEY_ADAPTIVE_SCHEDULING, false); ContentValues values = new ContentValues(); values.put(DailyNotificationDatabase.COL_CONFIG_K, KEY_ADAPTIVE_SCHEDULING); values.put(DailyNotificationDatabase.COL_CONFIG_V, String.valueOf(adaptiveScheduling)); long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values); if (rowId != -1) { migratedCount++; Log.d(TAG, "Migrated adaptive_scheduling setting"); } Log.i(TAG, "Migrated " + migratedCount + " settings to SQLite"); return migratedCount; } catch (Exception e) { Log.e(TAG, "Error migrating settings", e); return 0; } } /** * Mark migration as complete in the database * * @param db SQLite database instance */ private void markMigrationComplete(SQLiteDatabase db) { try { ContentValues values = new ContentValues(); values.put(DailyNotificationDatabase.COL_CONFIG_K, "migration_complete"); values.put(DailyNotificationDatabase.COL_CONFIG_V, String.valueOf(System.currentTimeMillis())); db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values); Log.d(TAG, "Migration marked as complete"); } catch (Exception e) { Log.e(TAG, "Error marking migration complete", e); } } /** * Validate migration success * * @return true if migration was successful */ public boolean validateMigration() { try { SQLiteDatabase db = database.getReadableDatabase(); // Check if migration_complete flag exists android.database.Cursor cursor = db.query( DailyNotificationDatabase.TABLE_NOTIF_CONFIG, new String[]{DailyNotificationDatabase.COL_CONFIG_V}, DailyNotificationDatabase.COL_CONFIG_K + " = ?", new String[]{"migration_complete"}, null, null, null ); boolean migrationComplete = cursor.moveToFirst(); cursor.close(); if (!migrationComplete) { Log.w(TAG, "Migration validation failed - migration_complete flag not found"); return false; } // Check if we have notification content cursor = db.rawQuery( "SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null); int contentCount = 0; if (cursor.moveToFirst()) { contentCount = cursor.getInt(0); } cursor.close(); Log.i(TAG, "Migration validation successful - " + contentCount + " notifications in SQLite"); return true; } catch (Exception e) { Log.e(TAG, "Error validating migration", e); return false; } } /** * Get migration statistics * * @return Migration statistics string */ public String getMigrationStats() { try { SQLiteDatabase db = database.getReadableDatabase(); // Count notifications android.database.Cursor cursor = db.rawQuery( "SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null); int notificationCount = 0; if (cursor.moveToFirst()) { notificationCount = cursor.getInt(0); } cursor.close(); // Count settings cursor = db.rawQuery( "SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null); int settingsCount = 0; if (cursor.moveToFirst()) { settingsCount = cursor.getInt(0); } cursor.close(); return String.format("Migration stats: %d notifications, %d settings", notificationCount, settingsCount); } catch (Exception e) { Log.e(TAG, "Error getting migration stats", e); return "Migration stats: Error retrieving data"; } } }