Browse Source
- DailyNotificationPlugin: inject Room storage into fetcher - DailyNotificationFetcher: persist to Room first, mirror to legacy - DailyNotificationWorker: read from Room, fallback to legacy; write next schedule to Room Legacy SharedPreferences path deprecated; retained for transitional compatibility. Co-authored-by: Matthew Raymermaster
7 changed files with 169 additions and 1285 deletions
@ -1,294 +0,0 @@ |
|||
package com.timesafari.dailynotification |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
import com.getcapacitor.JSObject |
|||
import com.getcapacitor.Plugin |
|||
import com.getcapacitor.PluginCall |
|||
import com.getcapacitor.PluginMethod |
|||
import com.getcapacitor.annotation.CapacitorPlugin |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
import org.json.JSONObject |
|||
|
|||
/** |
|||
* Main Android implementation of Daily Notification Plugin |
|||
* Bridges Capacitor calls to native Android functionality |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.1.0 |
|||
*/ |
|||
@CapacitorPlugin(name = "DailyNotification") |
|||
class DailyNotificationPlugin : Plugin() { |
|||
|
|||
companion object { |
|||
private const val TAG = "DNP-PLUGIN" |
|||
} |
|||
|
|||
private lateinit var db: DailyNotificationDatabase |
|||
|
|||
override fun load() { |
|||
super.load() |
|||
db = DailyNotificationDatabase.getDatabase(context) |
|||
Log.i(TAG, "Daily Notification Plugin loaded") |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun configure(call: PluginCall) { |
|||
try { |
|||
val options = call.getObject("options") |
|||
Log.i(TAG, "Configure called with options: $options") |
|||
|
|||
// Store configuration in database |
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
// Implementation would store config in database |
|||
call.resolve() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to configure", e) |
|||
call.reject("Configuration failed: ${e.message}") |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Configure error", e) |
|||
call.reject("Configuration error: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun scheduleContentFetch(call: PluginCall) { |
|||
try { |
|||
val configJson = call.getObject("config") |
|||
val config = parseContentFetchConfig(configJson) |
|||
|
|||
Log.i(TAG, "Scheduling content fetch") |
|||
|
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
// Schedule WorkManager fetch |
|||
FetchWorker.scheduleFetch(context, config) |
|||
|
|||
// Store schedule in database |
|||
val schedule = Schedule( |
|||
id = "fetch_${System.currentTimeMillis()}", |
|||
kind = "fetch", |
|||
cron = config.schedule, |
|||
enabled = config.enabled, |
|||
nextRunAt = calculateNextRunTime(config.schedule) |
|||
) |
|||
db.scheduleDao().upsert(schedule) |
|||
|
|||
call.resolve() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to schedule content fetch", e) |
|||
call.reject("Content fetch scheduling failed: ${e.message}") |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Schedule content fetch error", e) |
|||
call.reject("Content fetch error: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun scheduleUserNotification(call: PluginCall) { |
|||
try { |
|||
val configJson = call.getObject("config") |
|||
val config = parseUserNotificationConfig(configJson) |
|||
|
|||
Log.i(TAG, "Scheduling user notification") |
|||
|
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
val nextRunTime = calculateNextRunTime(config.schedule) |
|||
|
|||
// Schedule AlarmManager notification |
|||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config) |
|||
|
|||
// Store schedule in database |
|||
val schedule = Schedule( |
|||
id = "notify_${System.currentTimeMillis()}", |
|||
kind = "notify", |
|||
cron = config.schedule, |
|||
enabled = config.enabled, |
|||
nextRunAt = nextRunTime |
|||
) |
|||
db.scheduleDao().upsert(schedule) |
|||
|
|||
call.resolve() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to schedule user notification", e) |
|||
call.reject("User notification scheduling failed: ${e.message}") |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Schedule user notification error", e) |
|||
call.reject("User notification error: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun scheduleDualNotification(call: PluginCall) { |
|||
try { |
|||
val configJson = call.getObject("config") |
|||
val contentFetchConfig = parseContentFetchConfig(configJson.getObject("contentFetch")) |
|||
val userNotificationConfig = parseUserNotificationConfig(configJson.getObject("userNotification")) |
|||
|
|||
Log.i(TAG, "Scheduling dual notification") |
|||
|
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
// Schedule both fetch and notification |
|||
FetchWorker.scheduleFetch(context, contentFetchConfig) |
|||
|
|||
val nextRunTime = calculateNextRunTime(userNotificationConfig.schedule) |
|||
NotifyReceiver.scheduleExactNotification(context, nextRunTime, userNotificationConfig) |
|||
|
|||
// Store both schedules |
|||
val fetchSchedule = Schedule( |
|||
id = "dual_fetch_${System.currentTimeMillis()}", |
|||
kind = "fetch", |
|||
cron = contentFetchConfig.schedule, |
|||
enabled = contentFetchConfig.enabled, |
|||
nextRunAt = calculateNextRunTime(contentFetchConfig.schedule) |
|||
) |
|||
val notifySchedule = Schedule( |
|||
id = "dual_notify_${System.currentTimeMillis()}", |
|||
kind = "notify", |
|||
cron = userNotificationConfig.schedule, |
|||
enabled = userNotificationConfig.enabled, |
|||
nextRunAt = nextRunTime |
|||
) |
|||
|
|||
db.scheduleDao().upsert(fetchSchedule) |
|||
db.scheduleDao().upsert(notifySchedule) |
|||
|
|||
call.resolve() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to schedule dual notification", e) |
|||
call.reject("Dual notification scheduling failed: ${e.message}") |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Schedule dual notification error", e) |
|||
call.reject("Dual notification error: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun getDualScheduleStatus(call: PluginCall) { |
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
val enabledSchedules = db.scheduleDao().getEnabled() |
|||
val latestCache = db.contentCacheDao().getLatest() |
|||
val recentHistory = db.historyDao().getSince(System.currentTimeMillis() - (24 * 60 * 60 * 1000L)) |
|||
|
|||
val status = JSObject().apply { |
|||
put("nextRuns", enabledSchedules.map { it.nextRunAt }) |
|||
put("lastOutcomes", recentHistory.map { it.outcome }) |
|||
put("cacheAgeMs", latestCache?.let { System.currentTimeMillis() - it.fetchedAt }) |
|||
put("staleArmed", latestCache?.let { |
|||
System.currentTimeMillis() > (it.fetchedAt + it.ttlSeconds * 1000L) |
|||
} ?: true) |
|||
put("queueDepth", recentHistory.size) |
|||
} |
|||
|
|||
call.resolve(status) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to get dual schedule status", e) |
|||
call.reject("Status retrieval failed: ${e.message}") |
|||
} |
|||
} |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun registerCallback(call: PluginCall) { |
|||
try { |
|||
val name = call.getString("name") |
|||
val callback = call.getObject("callback") |
|||
|
|||
Log.i(TAG, "Registering callback: $name") |
|||
|
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
val callbackRecord = Callback( |
|||
id = name, |
|||
kind = callback.getString("kind", "local"), |
|||
target = callback.getString("target", ""), |
|||
headersJson = callback.getString("headers"), |
|||
enabled = true, |
|||
createdAt = System.currentTimeMillis() |
|||
) |
|||
|
|||
db.callbackDao().upsert(callbackRecord) |
|||
call.resolve() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to register callback", e) |
|||
call.reject("Callback registration failed: ${e.message}") |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Register callback error", e) |
|||
call.reject("Callback registration error: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
@PluginMethod |
|||
fun getContentCache(call: PluginCall) { |
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
try { |
|||
val latestCache = db.contentCacheDao().getLatest() |
|||
val result = JSObject() |
|||
|
|||
if (latestCache != null) { |
|||
result.put("id", latestCache.id) |
|||
result.put("fetchedAt", latestCache.fetchedAt) |
|||
result.put("ttlSeconds", latestCache.ttlSeconds) |
|||
result.put("payload", String(latestCache.payload)) |
|||
result.put("meta", latestCache.meta) |
|||
} |
|||
|
|||
call.resolve(result) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to get content cache", e) |
|||
call.reject("Content cache retrieval failed: ${e.message}") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Helper methods |
|||
private fun parseContentFetchConfig(configJson: JSObject): ContentFetchConfig { |
|||
return ContentFetchConfig( |
|||
enabled = configJson.getBoolean("enabled", true), |
|||
schedule = configJson.getString("schedule", "0 9 * * *"), |
|||
url = configJson.getString("url"), |
|||
timeout = configJson.getInt("timeout"), |
|||
retryAttempts = configJson.getInt("retryAttempts"), |
|||
retryDelay = configJson.getInt("retryDelay"), |
|||
callbacks = CallbackConfig( |
|||
apiService = configJson.getObject("callbacks")?.getString("apiService"), |
|||
database = configJson.getObject("callbacks")?.getString("database"), |
|||
reporting = configJson.getObject("callbacks")?.getString("reporting") |
|||
) |
|||
) |
|||
} |
|||
|
|||
private fun parseUserNotificationConfig(configJson: JSObject): UserNotificationConfig { |
|||
return UserNotificationConfig( |
|||
enabled = configJson.getBoolean("enabled", true), |
|||
schedule = configJson.getString("schedule", "0 9 * * *"), |
|||
title = configJson.getString("title"), |
|||
body = configJson.getString("body"), |
|||
sound = configJson.getBoolean("sound"), |
|||
vibration = configJson.getBoolean("vibration"), |
|||
priority = configJson.getString("priority") |
|||
) |
|||
} |
|||
|
|||
private fun calculateNextRunTime(schedule: String): Long { |
|||
// Simple implementation - for production, use proper cron parsing |
|||
val now = System.currentTimeMillis() |
|||
return now + (24 * 60 * 60 * 1000L) // Next day |
|||
} |
|||
} |
@ -1,312 +0,0 @@ |
|||
/** |
|||
* DailyNotificationDatabase.java |
|||
* |
|||
* SQLite database management for shared notification storage |
|||
* Implements the three-table schema with WAL mode for concurrent access |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
package com.timesafari.dailynotification; |
|||
|
|||
import android.content.Context; |
|||
import android.database.sqlite.SQLiteDatabase; |
|||
import android.database.sqlite.SQLiteOpenHelper; |
|||
import android.util.Log; |
|||
|
|||
import java.io.File; |
|||
|
|||
/** |
|||
* Manages SQLite database for shared notification storage |
|||
* |
|||
* This class implements the shared database approach where: |
|||
* - App owns schema/migrations (PRAGMA user_version) |
|||
* - Plugin opens the same path with WAL mode |
|||
* - Background writes are short & serialized |
|||
* - Foreground reads proceed during background commits |
|||
*/ |
|||
public class DailyNotificationDatabase extends SQLiteOpenHelper { |
|||
|
|||
private static final String TAG = "DailyNotificationDatabase"; |
|||
private static final String DATABASE_NAME = "daily_notifications.db"; |
|||
private static final int DATABASE_VERSION = 1; |
|||
|
|||
// Table names
|
|||
public static final String TABLE_NOTIF_CONTENTS = "notif_contents"; |
|||
public static final String TABLE_NOTIF_DELIVERIES = "notif_deliveries"; |
|||
public static final String TABLE_NOTIF_CONFIG = "notif_config"; |
|||
|
|||
// Column names for notif_contents
|
|||
public static final String COL_CONTENTS_ID = "id"; |
|||
public static final String COL_CONTENTS_SLOT_ID = "slot_id"; |
|||
public static final String COL_CONTENTS_PAYLOAD_JSON = "payload_json"; |
|||
public static final String COL_CONTENTS_FETCHED_AT = "fetched_at"; |
|||
public static final String COL_CONTENTS_ETAG = "etag"; |
|||
|
|||
// Column names for notif_deliveries
|
|||
public static final String COL_DELIVERIES_ID = "id"; |
|||
public static final String COL_DELIVERIES_SLOT_ID = "slot_id"; |
|||
public static final String COL_DELIVERIES_FIRE_AT = "fire_at"; |
|||
public static final String COL_DELIVERIES_DELIVERED_AT = "delivered_at"; |
|||
public static final String COL_DELIVERIES_STATUS = "status"; |
|||
public static final String COL_DELIVERIES_ERROR_CODE = "error_code"; |
|||
public static final String COL_DELIVERIES_ERROR_MESSAGE = "error_message"; |
|||
|
|||
// Column names for notif_config
|
|||
public static final String COL_CONFIG_K = "k"; |
|||
public static final String COL_CONFIG_V = "v"; |
|||
|
|||
// Status values
|
|||
public static final String STATUS_SCHEDULED = "scheduled"; |
|||
public static final String STATUS_SHOWN = "shown"; |
|||
public static final String STATUS_ERROR = "error"; |
|||
public static final String STATUS_CANCELED = "canceled"; |
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param context Application context |
|||
* @param dbPath Database file path (null for default location) |
|||
*/ |
|||
public DailyNotificationDatabase(Context context, String dbPath) { |
|||
super(context, dbPath != null ? dbPath : DATABASE_NAME, null, DATABASE_VERSION); |
|||
} |
|||
|
|||
/** |
|||
* Constructor with default database location |
|||
* |
|||
* @param context Application context |
|||
*/ |
|||
public DailyNotificationDatabase(Context context) { |
|||
this(context, null); |
|||
} |
|||
|
|||
@Override |
|||
public void onCreate(SQLiteDatabase db) { |
|||
Log.d(TAG, "Creating database tables"); |
|||
|
|||
// Configure database for WAL mode and concurrent access
|
|||
configureDatabase(db); |
|||
|
|||
// Create tables
|
|||
createTables(db); |
|||
|
|||
// Create indexes
|
|||
createIndexes(db); |
|||
|
|||
Log.i(TAG, "Database created successfully"); |
|||
} |
|||
|
|||
@Override |
|||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
|||
Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); |
|||
|
|||
// For now, drop and recreate tables
|
|||
// In production, implement proper migration logic
|
|||
dropTables(db); |
|||
onCreate(db); |
|||
|
|||
Log.i(TAG, "Database upgraded successfully"); |
|||
} |
|||
|
|||
@Override |
|||
public void onOpen(SQLiteDatabase db) { |
|||
super.onOpen(db); |
|||
|
|||
// Ensure WAL mode is enabled on every open
|
|||
configureDatabase(db); |
|||
|
|||
// Verify schema version
|
|||
verifySchemaVersion(db); |
|||
|
|||
Log.d(TAG, "Database opened with WAL mode"); |
|||
} |
|||
|
|||
/** |
|||
* Configure database for optimal performance and concurrency |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void configureDatabase(SQLiteDatabase db) { |
|||
// Enable WAL mode for concurrent reads during writes
|
|||
db.execSQL("PRAGMA journal_mode=WAL"); |
|||
|
|||
// Set synchronous mode to NORMAL for better performance
|
|||
db.execSQL("PRAGMA synchronous=NORMAL"); |
|||
|
|||
// Set busy timeout to handle concurrent access
|
|||
db.execSQL("PRAGMA busy_timeout=5000"); |
|||
|
|||
// Enable foreign key constraints
|
|||
db.execSQL("PRAGMA foreign_keys=ON"); |
|||
|
|||
// Set cache size for better performance
|
|||
db.execSQL("PRAGMA cache_size=1000"); |
|||
|
|||
Log.d(TAG, "Database configured with WAL mode and optimizations"); |
|||
} |
|||
|
|||
/** |
|||
* Create all database tables |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void createTables(SQLiteDatabase db) { |
|||
// notif_contents: keep history, fast newest-first reads
|
|||
String createContentsTable = String.format( |
|||
"CREATE TABLE IF NOT EXISTS %s(" + |
|||
"%s INTEGER PRIMARY KEY AUTOINCREMENT," + |
|||
"%s TEXT NOT NULL," + |
|||
"%s TEXT NOT NULL," + |
|||
"%s INTEGER NOT NULL," + // epoch ms
|
|||
"%s TEXT," + |
|||
"UNIQUE(%s, %s)" + |
|||
")", |
|||
TABLE_NOTIF_CONTENTS, |
|||
COL_CONTENTS_ID, |
|||
COL_CONTENTS_SLOT_ID, |
|||
COL_CONTENTS_PAYLOAD_JSON, |
|||
COL_CONTENTS_FETCHED_AT, |
|||
COL_CONTENTS_ETAG, |
|||
COL_CONTENTS_SLOT_ID, |
|||
COL_CONTENTS_FETCHED_AT |
|||
); |
|||
|
|||
// notif_deliveries: track many deliveries per slot/time
|
|||
String createDeliveriesTable = String.format( |
|||
"CREATE TABLE IF NOT EXISTS %s(" + |
|||
"%s INTEGER PRIMARY KEY AUTOINCREMENT," + |
|||
"%s TEXT NOT NULL," + |
|||
"%s INTEGER NOT NULL," + // epoch ms
|
|||
"%s INTEGER," + // epoch ms
|
|||
"%s TEXT NOT NULL DEFAULT '%s'," + |
|||
"%s TEXT," + |
|||
"%s TEXT" + |
|||
")", |
|||
TABLE_NOTIF_DELIVERIES, |
|||
COL_DELIVERIES_ID, |
|||
COL_DELIVERIES_SLOT_ID, |
|||
COL_DELIVERIES_FIRE_AT, |
|||
COL_DELIVERIES_DELIVERED_AT, |
|||
COL_DELIVERIES_STATUS, |
|||
STATUS_SCHEDULED, |
|||
COL_DELIVERIES_ERROR_CODE, |
|||
COL_DELIVERIES_ERROR_MESSAGE |
|||
); |
|||
|
|||
// notif_config: generic configuration KV
|
|||
String createConfigTable = String.format( |
|||
"CREATE TABLE IF NOT EXISTS %s(" + |
|||
"%s TEXT PRIMARY KEY," + |
|||
"%s TEXT NOT NULL" + |
|||
")", |
|||
TABLE_NOTIF_CONFIG, |
|||
COL_CONFIG_K, |
|||
COL_CONFIG_V |
|||
); |
|||
|
|||
db.execSQL(createContentsTable); |
|||
db.execSQL(createDeliveriesTable); |
|||
db.execSQL(createConfigTable); |
|||
|
|||
Log.d(TAG, "Database tables created"); |
|||
} |
|||
|
|||
/** |
|||
* Create database indexes for optimal query performance |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void createIndexes(SQLiteDatabase db) { |
|||
// Index for notif_contents: slot_id + fetched_at DESC for newest-first reads
|
|||
String createContentsIndex = String.format( |
|||
"CREATE INDEX IF NOT EXISTS notif_idx_contents_slot_time ON %s(%s, %s DESC)", |
|||
TABLE_NOTIF_CONTENTS, |
|||
COL_CONTENTS_SLOT_ID, |
|||
COL_CONTENTS_FETCHED_AT |
|||
); |
|||
|
|||
// Index for notif_deliveries: slot_id for delivery tracking
|
|||
String createDeliveriesIndex = String.format( |
|||
"CREATE INDEX IF NOT EXISTS notif_idx_deliveries_slot ON %s(%s)", |
|||
TABLE_NOTIF_DELIVERIES, |
|||
COL_DELIVERIES_SLOT_ID |
|||
); |
|||
|
|||
db.execSQL(createContentsIndex); |
|||
db.execSQL(createDeliveriesIndex); |
|||
|
|||
Log.d(TAG, "Database indexes created"); |
|||
} |
|||
|
|||
/** |
|||
* Drop all database tables (for migration) |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void dropTables(SQLiteDatabase db) { |
|||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_CONTENTS); |
|||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_DELIVERIES); |
|||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_CONFIG); |
|||
|
|||
Log.d(TAG, "Database tables dropped"); |
|||
} |
|||
|
|||
/** |
|||
* Verify schema version compatibility |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void verifySchemaVersion(SQLiteDatabase db) { |
|||
try { |
|||
// Get current user_version
|
|||
android.database.Cursor cursor = db.rawQuery("PRAGMA user_version", null); |
|||
int currentVersion = 0; |
|||
if (cursor.moveToFirst()) { |
|||
currentVersion = cursor.getInt(0); |
|||
} |
|||
cursor.close(); |
|||
|
|||
Log.d(TAG, "Current schema version: " + currentVersion); |
|||
|
|||
// Set user_version to match our DATABASE_VERSION
|
|||
db.execSQL("PRAGMA user_version=" + DATABASE_VERSION); |
|||
|
|||
Log.d(TAG, "Schema version verified and set to " + DATABASE_VERSION); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error verifying schema version", e); |
|||
throw new RuntimeException("Schema version verification failed", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get database file path |
|||
* |
|||
* @return Database file path |
|||
*/ |
|||
public String getDatabasePath() { |
|||
return getReadableDatabase().getPath(); |
|||
} |
|||
|
|||
/** |
|||
* Check if database file exists |
|||
* |
|||
* @return true if database file exists |
|||
*/ |
|||
public boolean databaseExists() { |
|||
File dbFile = new File(getDatabasePath()); |
|||
return dbFile.exists(); |
|||
} |
|||
|
|||
/** |
|||
* Get database size in bytes |
|||
* |
|||
* @return Database file size in bytes |
|||
*/ |
|||
public long getDatabaseSize() { |
|||
File dbFile = new File(getDatabasePath()); |
|||
return dbFile.exists() ? dbFile.length() : 0; |
|||
} |
|||
} |
@ -1,357 +0,0 @@ |
|||
/** |
|||
* TimeSafari Android Configuration |
|||
* |
|||
* Provides TimeSafari-specific Android platform configuration including |
|||
* notification channels, permissions, and battery optimization settings. |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
/** |
|||
* TimeSafari Android Configuration Interface |
|||
*/ |
|||
export interface TimeSafariAndroidConfig { |
|||
/** |
|||
* Notification channel configuration |
|||
*/ |
|||
notificationChannels: NotificationChannelConfig[]; |
|||
|
|||
/** |
|||
* Permission requirements |
|||
*/ |
|||
permissions: AndroidPermission[]; |
|||
|
|||
/** |
|||
* Battery optimization settings |
|||
*/ |
|||
batteryOptimization: BatteryOptimizationConfig; |
|||
|
|||
/** |
|||
* Doze and App Standby settings |
|||
*/ |
|||
powerManagement: PowerManagementConfig; |
|||
|
|||
/** |
|||
* WorkManager constraints |
|||
*/ |
|||
workManagerConstraints: WorkManagerConstraints; |
|||
} |
|||
|
|||
/** |
|||
* Notification Channel Configuration |
|||
*/ |
|||
export interface NotificationChannelConfig { |
|||
id: string; |
|||
name: string; |
|||
description: string; |
|||
importance: 'low' | 'default' | 'high' | 'max'; |
|||
enableLights: boolean; |
|||
enableVibration: boolean; |
|||
lightColor: string; |
|||
sound: string | null; |
|||
showBadge: boolean; |
|||
bypassDnd: boolean; |
|||
lockscreenVisibility: 'public' | 'private' | 'secret'; |
|||
} |
|||
|
|||
/** |
|||
* Android Permission Configuration |
|||
*/ |
|||
export interface AndroidPermission { |
|||
name: string; |
|||
description: string; |
|||
required: boolean; |
|||
runtime: boolean; |
|||
category: 'notification' | 'alarm' | 'network' | 'storage' | 'system'; |
|||
} |
|||
|
|||
/** |
|||
* Battery Optimization Configuration |
|||
*/ |
|||
export interface BatteryOptimizationConfig { |
|||
exemptPackages: string[]; |
|||
whitelistRequestMessage: string; |
|||
optimizationCheckInterval: number; // minutes
|
|||
fallbackBehavior: 'graceful' | 'aggressive' | 'disabled'; |
|||
} |
|||
|
|||
/** |
|||
* Power Management Configuration |
|||
*/ |
|||
export interface PowerManagementConfig { |
|||
dozeModeHandling: 'ignore' | 'adapt' | 'request_whitelist'; |
|||
appStandbyHandling: 'ignore' | 'adapt' | 'request_whitelist'; |
|||
backgroundRestrictions: 'ignore' | 'adapt' | 'request_whitelist'; |
|||
adaptiveBatteryHandling: 'ignore' | 'adapt' | 'request_whitelist'; |
|||
} |
|||
|
|||
/** |
|||
* WorkManager Constraints Configuration |
|||
*/ |
|||
export interface WorkManagerConstraints { |
|||
networkType: 'not_required' | 'connected' | 'unmetered' | 'not_roaming' | 'metered'; |
|||
requiresBatteryNotLow: boolean; |
|||
requiresCharging: boolean; |
|||
requiresDeviceIdle: boolean; |
|||
requiresStorageNotLow: boolean; |
|||
backoffPolicy: 'linear' | 'exponential'; |
|||
backoffDelay: number; // milliseconds
|
|||
maxRetries: number; |
|||
} |
|||
|
|||
/** |
|||
* Default TimeSafari Android Configuration |
|||
*/ |
|||
export const DEFAULT_TIMESAFARI_ANDROID_CONFIG: TimeSafariAndroidConfig = { |
|||
notificationChannels: [ |
|||
{ |
|||
id: 'timesafari_community_updates', |
|||
name: 'TimeSafari Community Updates', |
|||
description: 'Daily updates from your TimeSafari community including new offers, project updates, and trust network activities', |
|||
importance: 'default', |
|||
enableLights: true, |
|||
enableVibration: true, |
|||
lightColor: '#2196F3', |
|||
sound: 'default', |
|||
showBadge: true, |
|||
bypassDnd: false, |
|||
lockscreenVisibility: 'public' |
|||
}, |
|||
{ |
|||
id: 'timesafari_project_notifications', |
|||
name: 'TimeSafari Project Notifications', |
|||
description: 'Notifications about starred projects, funding updates, and project milestones', |
|||
importance: 'high', |
|||
enableLights: true, |
|||
enableVibration: true, |
|||
lightColor: '#4CAF50', |
|||
sound: 'default', |
|||
showBadge: true, |
|||
bypassDnd: false, |
|||
lockscreenVisibility: 'public' |
|||
}, |
|||
{ |
|||
id: 'timesafari_trust_network', |
|||
name: 'TimeSafari Trust Network', |
|||
description: 'Trust network activities, endorsements, and community recommendations', |
|||
importance: 'default', |
|||
enableLights: true, |
|||
enableVibration: false, |
|||
lightColor: '#FF9800', |
|||
sound: null, |
|||
showBadge: true, |
|||
bypassDnd: false, |
|||
lockscreenVisibility: 'public' |
|||
}, |
|||
{ |
|||
id: 'timesafari_system', |
|||
name: 'TimeSafari System', |
|||
description: 'System notifications, authentication updates, and plugin status messages', |
|||
importance: 'low', |
|||
enableLights: false, |
|||
enableVibration: false, |
|||
lightColor: '#9E9E9E', |
|||
sound: null, |
|||
showBadge: false, |
|||
bypassDnd: false, |
|||
lockscreenVisibility: 'private' |
|||
}, |
|||
{ |
|||
id: 'timesafari_reminders', |
|||
name: 'TimeSafari Reminders', |
|||
description: 'Personal reminders and daily check-ins for your TimeSafari activities', |
|||
importance: 'default', |
|||
enableLights: true, |
|||
enableVibration: true, |
|||
lightColor: '#9C27B0', |
|||
sound: 'default', |
|||
showBadge: true, |
|||
bypassDnd: false, |
|||
lockscreenVisibility: 'public' |
|||
} |
|||
], |
|||
|
|||
permissions: [ |
|||
{ |
|||
name: 'android.permission.POST_NOTIFICATIONS', |
|||
description: 'Allow TimeSafari to show notifications', |
|||
required: true, |
|||
runtime: true, |
|||
category: 'notification' |
|||
}, |
|||
{ |
|||
name: 'android.permission.SCHEDULE_EXACT_ALARM', |
|||
description: 'Allow TimeSafari to schedule exact alarms for notifications', |
|||
required: true, |
|||
runtime: true, |
|||
category: 'alarm' |
|||
}, |
|||
{ |
|||
name: 'android.permission.USE_EXACT_ALARM', |
|||
description: 'Allow TimeSafari to use exact alarms', |
|||
required: false, |
|||
runtime: false, |
|||
category: 'alarm' |
|||
}, |
|||
{ |
|||
name: 'android.permission.WAKE_LOCK', |
|||
description: 'Allow TimeSafari to keep device awake for background tasks', |
|||
required: true, |
|||
runtime: false, |
|||
category: 'system' |
|||
}, |
|||
{ |
|||
name: 'android.permission.RECEIVE_BOOT_COMPLETED', |
|||
description: 'Allow TimeSafari to restart notifications after device reboot', |
|||
required: true, |
|||
runtime: false, |
|||
category: 'system' |
|||
}, |
|||
{ |
|||
name: 'android.permission.INTERNET', |
|||
description: 'Allow TimeSafari to fetch community data and send callbacks', |
|||
required: true, |
|||
runtime: false, |
|||
category: 'network' |
|||
}, |
|||
{ |
|||
name: 'android.permission.ACCESS_NETWORK_STATE', |
|||
description: 'Allow TimeSafari to check network connectivity', |
|||
required: true, |
|||
runtime: false, |
|||
category: 'network' |
|||
} |
|||
], |
|||
|
|||
batteryOptimization: { |
|||
exemptPackages: ['com.timesafari.dailynotification'], |
|||
whitelistRequestMessage: 'TimeSafari needs to run in the background to deliver your daily community updates and notifications. Please whitelist TimeSafari from battery optimization.', |
|||
optimizationCheckInterval: 60, // 1 hour
|
|||
fallbackBehavior: 'graceful' |
|||
}, |
|||
|
|||
powerManagement: { |
|||
dozeModeHandling: 'request_whitelist', |
|||
appStandbyHandling: 'request_whitelist', |
|||
backgroundRestrictions: 'request_whitelist', |
|||
adaptiveBatteryHandling: 'request_whitelist' |
|||
}, |
|||
|
|||
workManagerConstraints: { |
|||
networkType: 'connected', |
|||
requiresBatteryNotLow: false, |
|||
requiresCharging: false, |
|||
requiresDeviceIdle: false, |
|||
requiresStorageNotLow: true, |
|||
backoffPolicy: 'exponential', |
|||
backoffDelay: 30000, // 30 seconds
|
|||
maxRetries: 3 |
|||
} |
|||
}; |
|||
|
|||
/** |
|||
* TimeSafari Android Configuration Manager |
|||
*/ |
|||
export class TimeSafariAndroidConfigManager { |
|||
private config: TimeSafariAndroidConfig; |
|||
|
|||
constructor(config?: Partial<TimeSafariAndroidConfig>) { |
|||
this.config = { ...DEFAULT_TIMESAFARI_ANDROID_CONFIG, ...config }; |
|||
} |
|||
|
|||
/** |
|||
* Get notification channel configuration |
|||
*/ |
|||
getNotificationChannel(channelId: string): NotificationChannelConfig | undefined { |
|||
return this.config.notificationChannels.find(channel => channel.id === channelId); |
|||
} |
|||
|
|||
/** |
|||
* Get all notification channels |
|||
*/ |
|||
getAllNotificationChannels(): NotificationChannelConfig[] { |
|||
return this.config.notificationChannels; |
|||
} |
|||
|
|||
/** |
|||
* Get required permissions |
|||
*/ |
|||
getRequiredPermissions(): AndroidPermission[] { |
|||
return this.config.permissions.filter(permission => permission.required); |
|||
} |
|||
|
|||
/** |
|||
* Get runtime permissions |
|||
*/ |
|||
getRuntimePermissions(): AndroidPermission[] { |
|||
return this.config.permissions.filter(permission => permission.runtime); |
|||
} |
|||
|
|||
/** |
|||
* Get permissions by category |
|||
*/ |
|||
getPermissionsByCategory(category: string): AndroidPermission[] { |
|||
return this.config.permissions.filter(permission => permission.category === category); |
|||
} |
|||
|
|||
/** |
|||
* Get battery optimization configuration |
|||
*/ |
|||
getBatteryOptimizationConfig(): BatteryOptimizationConfig { |
|||
return this.config.batteryOptimization; |
|||
} |
|||
|
|||
/** |
|||
* Get power management configuration |
|||
*/ |
|||
getPowerManagementConfig(): PowerManagementConfig { |
|||
return this.config.powerManagement; |
|||
} |
|||
|
|||
/** |
|||
* Get WorkManager constraints |
|||
*/ |
|||
getWorkManagerConstraints(): WorkManagerConstraints { |
|||
return this.config.workManagerConstraints; |
|||
} |
|||
|
|||
/** |
|||
* Update configuration |
|||
*/ |
|||
updateConfig(updates: Partial<TimeSafariAndroidConfig>): void { |
|||
this.config = { ...this.config, ...updates }; |
|||
} |
|||
|
|||
/** |
|||
* Validate configuration |
|||
*/ |
|||
validateConfig(): { valid: boolean; errors: string[] } { |
|||
const errors: string[] = []; |
|||
|
|||
// Validate notification channels
|
|||
if (this.config.notificationChannels.length === 0) { |
|||
errors.push('At least one notification channel must be configured'); |
|||
} |
|||
|
|||
// Validate permissions
|
|||
const requiredPermissions = this.getRequiredPermissions(); |
|||
if (requiredPermissions.length === 0) { |
|||
errors.push('At least one required permission must be configured'); |
|||
} |
|||
|
|||
// Validate WorkManager constraints
|
|||
if (this.config.workManagerConstraints.maxRetries < 0) { |
|||
errors.push('WorkManager maxRetries must be non-negative'); |
|||
} |
|||
|
|||
if (this.config.workManagerConstraints.backoffDelay < 0) { |
|||
errors.push('WorkManager backoffDelay must be non-negative'); |
|||
} |
|||
|
|||
return { |
|||
valid: errors.length === 0, |
|||
errors |
|||
}; |
|||
} |
|||
} |
@ -1,312 +0,0 @@ |
|||
/** |
|||
* DailyNotificationDatabase.java |
|||
* |
|||
* SQLite database management for shared notification storage |
|||
* Implements the three-table schema with WAL mode for concurrent access |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
package com.timesafari.dailynotification; |
|||
|
|||
import android.content.Context; |
|||
import android.database.sqlite.SQLiteDatabase; |
|||
import android.database.sqlite.SQLiteOpenHelper; |
|||
import android.util.Log; |
|||
|
|||
import java.io.File; |
|||
|
|||
/** |
|||
* Manages SQLite database for shared notification storage |
|||
* |
|||
* This class implements the shared database approach where: |
|||
* - App owns schema/migrations (PRAGMA user_version) |
|||
* - Plugin opens the same path with WAL mode |
|||
* - Background writes are short & serialized |
|||
* - Foreground reads proceed during background commits |
|||
*/ |
|||
public class DailyNotificationDatabase extends SQLiteOpenHelper { |
|||
|
|||
private static final String TAG = "DailyNotificationDatabase"; |
|||
private static final String DATABASE_NAME = "daily_notifications.db"; |
|||
private static final int DATABASE_VERSION = 1; |
|||
|
|||
// Table names
|
|||
public static final String TABLE_NOTIF_CONTENTS = "notif_contents"; |
|||
public static final String TABLE_NOTIF_DELIVERIES = "notif_deliveries"; |
|||
public static final String TABLE_NOTIF_CONFIG = "notif_config"; |
|||
|
|||
// Column names for notif_contents
|
|||
public static final String COL_CONTENTS_ID = "id"; |
|||
public static final String COL_CONTENTS_SLOT_ID = "slot_id"; |
|||
public static final String COL_CONTENTS_PAYLOAD_JSON = "payload_json"; |
|||
public static final String COL_CONTENTS_FETCHED_AT = "fetched_at"; |
|||
public static final String COL_CONTENTS_ETAG = "etag"; |
|||
|
|||
// Column names for notif_deliveries
|
|||
public static final String COL_DELIVERIES_ID = "id"; |
|||
public static final String COL_DELIVERIES_SLOT_ID = "slot_id"; |
|||
public static final String COL_DELIVERIES_FIRE_AT = "fire_at"; |
|||
public static final String COL_DELIVERIES_DELIVERED_AT = "delivered_at"; |
|||
public static final String COL_DELIVERIES_STATUS = "status"; |
|||
public static final String COL_DELIVERIES_ERROR_CODE = "error_code"; |
|||
public static final String COL_DELIVERIES_ERROR_MESSAGE = "error_message"; |
|||
|
|||
// Column names for notif_config
|
|||
public static final String COL_CONFIG_K = "k"; |
|||
public static final String COL_CONFIG_V = "v"; |
|||
|
|||
// Status values
|
|||
public static final String STATUS_SCHEDULED = "scheduled"; |
|||
public static final String STATUS_SHOWN = "shown"; |
|||
public static final String STATUS_ERROR = "error"; |
|||
public static final String STATUS_CANCELED = "canceled"; |
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param context Application context |
|||
* @param dbPath Database file path (null for default location) |
|||
*/ |
|||
public DailyNotificationDatabase(Context context, String dbPath) { |
|||
super(context, dbPath != null ? dbPath : DATABASE_NAME, null, DATABASE_VERSION); |
|||
} |
|||
|
|||
/** |
|||
* Constructor with default database location |
|||
* |
|||
* @param context Application context |
|||
*/ |
|||
public DailyNotificationDatabase(Context context) { |
|||
this(context, null); |
|||
} |
|||
|
|||
@Override |
|||
public void onCreate(SQLiteDatabase db) { |
|||
Log.d(TAG, "Creating database tables"); |
|||
|
|||
// Configure database for WAL mode and concurrent access
|
|||
configureDatabase(db); |
|||
|
|||
// Create tables
|
|||
createTables(db); |
|||
|
|||
// Create indexes
|
|||
createIndexes(db); |
|||
|
|||
Log.i(TAG, "Database created successfully"); |
|||
} |
|||
|
|||
@Override |
|||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
|||
Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); |
|||
|
|||
// For now, drop and recreate tables
|
|||
// In production, implement proper migration logic
|
|||
dropTables(db); |
|||
onCreate(db); |
|||
|
|||
Log.i(TAG, "Database upgraded successfully"); |
|||
} |
|||
|
|||
@Override |
|||
public void onOpen(SQLiteDatabase db) { |
|||
super.onOpen(db); |
|||
|
|||
// Ensure WAL mode is enabled on every open
|
|||
configureDatabase(db); |
|||
|
|||
// Verify schema version
|
|||
verifySchemaVersion(db); |
|||
|
|||
Log.d(TAG, "Database opened with WAL mode"); |
|||
} |
|||
|
|||
/** |
|||
* Configure database for optimal performance and concurrency |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void configureDatabase(SQLiteDatabase db) { |
|||
// Enable WAL mode for concurrent reads during writes
|
|||
db.execSQL("PRAGMA journal_mode=WAL"); |
|||
|
|||
// Set synchronous mode to NORMAL for better performance
|
|||
db.execSQL("PRAGMA synchronous=NORMAL"); |
|||
|
|||
// Set busy timeout to handle concurrent access
|
|||
db.execSQL("PRAGMA busy_timeout=5000"); |
|||
|
|||
// Enable foreign key constraints
|
|||
db.execSQL("PRAGMA foreign_keys=ON"); |
|||
|
|||
// Set cache size for better performance
|
|||
db.execSQL("PRAGMA cache_size=1000"); |
|||
|
|||
Log.d(TAG, "Database configured with WAL mode and optimizations"); |
|||
} |
|||
|
|||
/** |
|||
* Create all database tables |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void createTables(SQLiteDatabase db) { |
|||
// notif_contents: keep history, fast newest-first reads
|
|||
String createContentsTable = String.format( |
|||
"CREATE TABLE IF NOT EXISTS %s(" + |
|||
"%s INTEGER PRIMARY KEY AUTOINCREMENT," + |
|||
"%s TEXT NOT NULL," + |
|||
"%s TEXT NOT NULL," + |
|||
"%s INTEGER NOT NULL," + // epoch ms
|
|||
"%s TEXT," + |
|||
"UNIQUE(%s, %s)" + |
|||
")", |
|||
TABLE_NOTIF_CONTENTS, |
|||
COL_CONTENTS_ID, |
|||
COL_CONTENTS_SLOT_ID, |
|||
COL_CONTENTS_PAYLOAD_JSON, |
|||
COL_CONTENTS_FETCHED_AT, |
|||
COL_CONTENTS_ETAG, |
|||
COL_CONTENTS_SLOT_ID, |
|||
COL_CONTENTS_FETCHED_AT |
|||
); |
|||
|
|||
// notif_deliveries: track many deliveries per slot/time
|
|||
String createDeliveriesTable = String.format( |
|||
"CREATE TABLE IF NOT EXISTS %s(" + |
|||
"%s INTEGER PRIMARY KEY AUTOINCREMENT," + |
|||
"%s TEXT NOT NULL," + |
|||
"%s INTEGER NOT NULL," + // epoch ms
|
|||
"%s INTEGER," + // epoch ms
|
|||
"%s TEXT NOT NULL DEFAULT '%s'," + |
|||
"%s TEXT," + |
|||
"%s TEXT" + |
|||
")", |
|||
TABLE_NOTIF_DELIVERIES, |
|||
COL_DELIVERIES_ID, |
|||
COL_DELIVERIES_SLOT_ID, |
|||
COL_DELIVERIES_FIRE_AT, |
|||
COL_DELIVERIES_DELIVERED_AT, |
|||
COL_DELIVERIES_STATUS, |
|||
STATUS_SCHEDULED, |
|||
COL_DELIVERIES_ERROR_CODE, |
|||
COL_DELIVERIES_ERROR_MESSAGE |
|||
); |
|||
|
|||
// notif_config: generic configuration KV
|
|||
String createConfigTable = String.format( |
|||
"CREATE TABLE IF NOT EXISTS %s(" + |
|||
"%s TEXT PRIMARY KEY," + |
|||
"%s TEXT NOT NULL" + |
|||
")", |
|||
TABLE_NOTIF_CONFIG, |
|||
COL_CONFIG_K, |
|||
COL_CONFIG_V |
|||
); |
|||
|
|||
db.execSQL(createContentsTable); |
|||
db.execSQL(createDeliveriesTable); |
|||
db.execSQL(createConfigTable); |
|||
|
|||
Log.d(TAG, "Database tables created"); |
|||
} |
|||
|
|||
/** |
|||
* Create database indexes for optimal query performance |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void createIndexes(SQLiteDatabase db) { |
|||
// Index for notif_contents: slot_id + fetched_at DESC for newest-first reads
|
|||
String createContentsIndex = String.format( |
|||
"CREATE INDEX IF NOT EXISTS notif_idx_contents_slot_time ON %s(%s, %s DESC)", |
|||
TABLE_NOTIF_CONTENTS, |
|||
COL_CONTENTS_SLOT_ID, |
|||
COL_CONTENTS_FETCHED_AT |
|||
); |
|||
|
|||
// Index for notif_deliveries: slot_id for delivery tracking
|
|||
String createDeliveriesIndex = String.format( |
|||
"CREATE INDEX IF NOT EXISTS notif_idx_deliveries_slot ON %s(%s)", |
|||
TABLE_NOTIF_DELIVERIES, |
|||
COL_DELIVERIES_SLOT_ID |
|||
); |
|||
|
|||
db.execSQL(createContentsIndex); |
|||
db.execSQL(createDeliveriesIndex); |
|||
|
|||
Log.d(TAG, "Database indexes created"); |
|||
} |
|||
|
|||
/** |
|||
* Drop all database tables (for migration) |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void dropTables(SQLiteDatabase db) { |
|||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_CONTENTS); |
|||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_DELIVERIES); |
|||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTIF_CONFIG); |
|||
|
|||
Log.d(TAG, "Database tables dropped"); |
|||
} |
|||
|
|||
/** |
|||
* Verify schema version compatibility |
|||
* |
|||
* @param db SQLite database instance |
|||
*/ |
|||
private void verifySchemaVersion(SQLiteDatabase db) { |
|||
try { |
|||
// Get current user_version
|
|||
android.database.Cursor cursor = db.rawQuery("PRAGMA user_version", null); |
|||
int currentVersion = 0; |
|||
if (cursor.moveToFirst()) { |
|||
currentVersion = cursor.getInt(0); |
|||
} |
|||
cursor.close(); |
|||
|
|||
Log.d(TAG, "Current schema version: " + currentVersion); |
|||
|
|||
// Set user_version to match our DATABASE_VERSION
|
|||
db.execSQL("PRAGMA user_version=" + DATABASE_VERSION); |
|||
|
|||
Log.d(TAG, "Schema version verified and set to " + DATABASE_VERSION); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error verifying schema version", e); |
|||
throw new RuntimeException("Schema version verification failed", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get database file path |
|||
* |
|||
* @return Database file path |
|||
*/ |
|||
public String getDatabasePath() { |
|||
return getReadableDatabase().getPath(); |
|||
} |
|||
|
|||
/** |
|||
* Check if database file exists |
|||
* |
|||
* @return true if database file exists |
|||
*/ |
|||
public boolean databaseExists() { |
|||
File dbFile = new File(getDatabasePath()); |
|||
return dbFile.exists(); |
|||
} |
|||
|
|||
/** |
|||
* Get database size in bytes |
|||
* |
|||
* @return Database file size in bytes |
|||
*/ |
|||
public long getDatabaseSize() { |
|||
File dbFile = new File(getDatabasePath()); |
|||
return dbFile.exists() ? dbFile.length() : 0; |
|||
} |
|||
} |
Loading…
Reference in new issue