From 4bbb1c0e45f55b93b9872baec213038e54cb820f Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:37:44 +0200 Subject: [PATCH 1/7] added new buttons for import/export with icons in navigation drawer --- app/src/main/res/drawable/ic_export_arrow.xml | 9 +++++++++ app/src/main/res/drawable/ic_history_db.xml | 13 +++++++++++++ app/src/main/res/drawable/ic_import_arrow.xml | 9 +++++++++ app/src/main/res/drawable/ic_star_db.xml | 11 +++++++++++ .../main/res/menu/activity_navigation_drawer.xml | 12 ++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 app/src/main/res/drawable/ic_export_arrow.xml create mode 100644 app/src/main/res/drawable/ic_history_db.xml create mode 100644 app/src/main/res/drawable/ic_import_arrow.xml create mode 100644 app/src/main/res/drawable/ic_star_db.xml diff --git a/app/src/main/res/drawable/ic_export_arrow.xml b/app/src/main/res/drawable/ic_export_arrow.xml new file mode 100644 index 0000000..ae7ec41 --- /dev/null +++ b/app/src/main/res/drawable/ic_export_arrow.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_history_db.xml b/app/src/main/res/drawable/ic_history_db.xml new file mode 100644 index 0000000..dcff018 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_db.xml @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_import_arrow.xml b/app/src/main/res/drawable/ic_import_arrow.xml new file mode 100644 index 0000000..cde20bd --- /dev/null +++ b/app/src/main/res/drawable/ic_import_arrow.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_db.xml b/app/src/main/res/drawable/ic_star_db.xml new file mode 100644 index 0000000..e8fa95c --- /dev/null +++ b/app/src/main/res/drawable/ic_star_db.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_navigation_drawer.xml b/app/src/main/res/menu/activity_navigation_drawer.xml index ab0fa1a..d1d5330 100644 --- a/app/src/main/res/menu/activity_navigation_drawer.xml +++ b/app/src/main/res/menu/activity_navigation_drawer.xml @@ -44,4 +44,16 @@ android:title="@string/drawer_centers" /> + + + + From 3040ceb2e25e2a6e2b974bd5e4f67c094a79b526 Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:38:58 +0200 Subject: [PATCH 2/7] - moved functionality to copy input stream into output stream to separate function - added functions to import/export history and star tables to external sqlite3 file --- .../org/dharmaseed/android/DBManager.java | 114 +++++++++++++++++- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/dharmaseed/android/DBManager.java b/app/src/main/java/org/dharmaseed/android/DBManager.java index 4034984..67bab12 100644 --- a/app/src/main/java/org/dharmaseed/android/DBManager.java +++ b/app/src/main/java/org/dharmaseed/android/DBManager.java @@ -25,12 +25,15 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import androidx.annotation.NonNull; + +import android.net.Uri; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -88,16 +91,21 @@ protected void copyAssetDB(File destFile) throws IOException { InputStream dbIn = context.getAssets().open(DB_NAME); destFile.getParentFile().mkdirs(); OutputStream dbOut = new FileOutputStream(destFile); + copyStreams(dbIn, dbOut, true); + } + private void copyStreams(InputStream src, OutputStream dest, boolean closeAfterCopy) throws IOException { byte[] buf = new byte[1024]; int len; - while ((len = dbIn.read(buf)) > 0) { - dbOut.write(buf, 0, len); + while ((len = src.read(buf)) > 0) { + dest.write(buf, 0, len); } + dest.flush(); - dbOut.flush(); - dbOut.close(); - dbIn.close(); + if (closeAfterCopy) { + src.close(); + dest.close(); + } } public static synchronized DBManager getInstance(Context context) { @@ -415,6 +423,102 @@ public boolean shouldSync() { return outOfDate; } + public void exportUserTablesToUri(Uri uri) throws IOException { + File tempFile = File.createTempFile("export", ".db", context.getCacheDir()); + SQLiteDatabase targetDb = SQLiteDatabase.openOrCreateDatabase(tempFile, null); + File sourceDbPath = context.getDatabasePath(DB_NAME); + + String[][] userTableSpec = { + {C.TalkStars.TABLE_NAME, C.TalkStars.CREATE_TABLE}, + {C.TeacherStars.TABLE_NAME, C.TeacherStars.CREATE_TABLE}, + {C.CenterStars.TABLE_NAME, C.CenterStars.CREATE_TABLE}, + {C.TalkHistory.TABLE_NAME, C.TalkHistory.CREATE_TABLE} + }; + for (String [] userTable: userTableSpec) { + String tableName = userTable[0], tableCreate = userTable[1]; + targetDb.execSQL(tableCreate); + targetDb.execSQL( + "ATTACH DATABASE ? AS source_db", + new Object[]{sourceDbPath} + ); + + // Copy everything in one go + targetDb.execSQL( + "INSERT INTO main." + tableName + " SELECT * FROM source_db." + tableName + ); + + // Detach again + targetDb.execSQL("DETACH DATABASE source_db"); + } + targetDb.close(); + + // 4. Write DB file to SAF Uri + copyStreams( + new FileInputStream(tempFile), + context.getContentResolver().openOutputStream(uri), + true + ); + + tempFile.delete(); + } + + public void importUserTablesFromUri(Uri uri) throws IOException { + SQLiteDatabase db = getWritableDatabase(); + // Create a temporary file to hold the database being imported + File tempFile = File.createTempFile("import", ".db", context.getCacheDir()); + + try { + // 1. Copy URI content to a temporary file using the existing copyStreams utility + InputStream is = context.getContentResolver().openInputStream(uri); + if (is == null) throw new IOException("Could not open input stream from URI: " + uri); + copyStreams(is, new FileOutputStream(tempFile), true); + + // 2. Attach the temporary database + db.execSQL("ATTACH DATABASE '" + tempFile.getAbsolutePath() + "' AS import_db"); + + try { + db.beginTransaction(); + + // 3. Handle the three Stars tables (Union) + // Since these only have one column (_id), INSERT OR IGNORE effectively performs a union + String[] starTables = { + C.TalkStars.TABLE_NAME, + C.TeacherStars.TABLE_NAME, + C.CenterStars.TABLE_NAME + }; + + for (String table : starTables) { + db.execSQL("INSERT OR IGNORE INTO main." + table + " SELECT * FROM import_db." + table); + } + + // A. Delete local rows where the imported database has a more recent DATE_TIME + final String historyTable = C.TalkHistory.TABLE_NAME; + final String idCol = C.TalkHistory.ID; + final String dateCol = C.TalkHistory.DATE_TIME; + + db.execSQL("DELETE FROM main." + historyTable + " WHERE " + idCol + " IN (" + + "SELECT imported." + idCol + " FROM import_db." + historyTable + " AS imported " + + "WHERE imported." + dateCol + " > main." + historyTable + "." + dateCol + ")"); + + // B. Insert all rows from the imported table that don't exist locally + // (This includes the ones we just deleted and brand new ones) + db.execSQL("INSERT OR IGNORE INTO main." + historyTable + + " SELECT * FROM import_db." + historyTable); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + // 5. Detach the database + db.execSQL("DETACH DATABASE import_db"); + } + } finally { + // Ensure the temporary file is cleaned up + if (tempFile.exists()) { + tempFile.delete(); + } + } + } + /** * Get an alias for a fully qualified column name. This is useful in naming columns in a query * using SQL AS clauses and referencing them later From d3fd24ee09456d8e957961232dcdbfef6bf6a9d6 Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:41:17 +0200 Subject: [PATCH 3/7] - moved initialisation of navigation drawer to NavigationActivity::initNavigationDrawer - lots of code just to add icons to the new import/export buttons :) - new functions for importing/exporting user data to storage using the android file picker and new functionality in DBManager --- .../android/NavigationActivity.java | 141 +++++++++++++++++- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/dharmaseed/android/NavigationActivity.java b/app/src/main/java/org/dharmaseed/android/NavigationActivity.java index 0b76f1a..a64ede2 100644 --- a/app/src/main/java/org/dharmaseed/android/NavigationActivity.java +++ b/app/src/main/java/org/dharmaseed/android/NavigationActivity.java @@ -28,6 +28,9 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.cursoradapter.widget.CursorAdapter; @@ -35,6 +38,11 @@ import android.text.Html; import android.text.method.LinkMovementMethod; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; +import androidx.core.content.ContextCompat; +import android.graphics.drawable.Drawable; import android.view.KeyEvent; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; @@ -57,6 +65,7 @@ import android.widget.TextView; import android.widget.Toast; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.Arrays; @@ -70,6 +79,7 @@ public class NavigationActivity extends AppCompatActivity View.OnFocusChangeListener { public final static String TALK_DETAIL_EXTRA = "org.dharmaseed.android.TALK_DETAIL"; + private final static String USER_DB_EXPORT_FILE = "dharmaseed_user_data.sqlite3"; NavigationView navigationView; ListView listView; @@ -260,11 +270,7 @@ public void onRefresh() { } }); - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( - this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); - drawer.setDrawerListener(toggle); - toggle.syncState(); + initNavigationDrawer(toolbar); LocalBroadcastManager.getInstance(this).registerReceiver(new BroadcastReceiver() { @Override @@ -287,6 +293,48 @@ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCoun }); } + private void initNavigationDrawer(Toolbar toolbar) { + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawer.addDrawerListener(toggle); + toggle.syncState(); + + addDBIcons( + navigationView.getMenu().findItem(R.id.nav_export), + getString(R.string.drawer_export) + ); + + addDBIcons( + navigationView.getMenu().findItem(R.id.nav_import), + getString(R.string.drawer_import) + ); + } + + private void addDBIcons(MenuItem item, String baseText) { + if (item == null) return; + SpannableStringBuilder sb = new SpannableStringBuilder(baseText + " & "); + int iconSize = (int) (headerPrimary.getTextSize() * 0.9); + addIconToSpan(sb, R.drawable.ic_history_db, baseText.length() + 1, iconSize); + addIconToSpan(sb, R.drawable.ic_star_db, baseText.length() + 3, iconSize); + item.setTitle(sb); + } + + private void addIconToSpan(SpannableStringBuilder sb, int drawableId, int index, int size) { + Drawable drawable = ContextCompat.getDrawable(this, drawableId); + if (drawable != null) { + drawable.setBounds(0, 0, size, size); + + // don't insert beyond the end of the string + if (index >= sb.length()) { + index = sb.length() - 1; + } + + ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); + sb.setSpan(imageSpan, index, index + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + private void updateScrollLabel(View item) { int scrollId = -1; ViewMode v = getCurrentViewMode(); @@ -709,12 +757,19 @@ public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); + boolean highlightItem = true; if (id == R.id.nav_talks) { setViewMode(new ViewMode(ViewMode.VIEW_MODE_TALKS)); } else if (id == R.id.nav_teachers) { setViewMode(new ViewMode(ViewMode.VIEW_MODE_TEACHERS)); } else if (id == R.id.nav_centers) { setViewMode(new ViewMode(ViewMode.VIEW_MODE_CENTERS)); + } else if (id == R.id.nav_export) { + startUserDBExport(); + highlightItem = false; + } else if (id == R.id.nav_import) { + startUserDBImport(); + highlightItem = false; } // else if (id == R.id.nav_retreats) { // Intent intent = new Intent(this, RetreatSearchActivity.class); @@ -723,7 +778,7 @@ public boolean onNavigationItemSelected(MenuItem item) { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); - return true; + return highlightItem; } public void headingDetailCollapseExpandButtonClicked(View view) { @@ -868,6 +923,80 @@ private List getSearchTerms() return searchTerms; } + private final ActivityResultLauncher exportDatabaseLauncher = registerForActivityResult( + new ActivityResultContracts.CreateDocument("application/x-sqlite3"), + uri -> { + if (uri != null) { + performUserDBExport(uri); + } + } + ); + + private void performUserDBExport(Uri uri) { + // We use a new thread because DB operations and file copying (I/O) + // in DBManager.exportUserTablesToUri will block the UI thread. + new Thread(() -> { + try { + dbManager.exportUserTablesToUri(uri); + Log.i(LOG_TAG, "Successfully exported user DB to " + uri); + + // Success: Switch back to UI thread to show toast + runOnUiThread(() -> showToast(getString(R.string.export_success))); + } catch (IOException e) { + Log.e(LOG_TAG, "Export failed", e); + + // Error: Switch back to UI thread to show toast + runOnUiThread(() -> showToast(getString(R.string.export_failed))); + } + }).start(); + } + + private void startUserDBExport() { + // The "CreateDocument" contract opens the file picker + // We pass the suggested filename here + exportDatabaseLauncher.launch(USER_DB_EXPORT_FILE); + } + + // 1. Add the launcher for picking a file + private final ActivityResultLauncher importDatabaseLauncher = registerForActivityResult( + new ActivityResultContracts.OpenDocument(), + uri -> { + if (uri != null) { + performUserDBImport(uri); + } + } + ); + + // 2. Implement the background processing logic + private void performUserDBImport(Uri uri) { + // We use a new thread because DB operations and file copying (I/O) + // in DBManager.importUserTablesFromUri will block the UI thread. + new Thread(() -> { + try { + dbManager.importUserTablesFromUri(uri); + Log.i(LOG_TAG, "Successfully imported user DB from " + uri); + + // Success: Switch back to UI thread to refresh data and show toast + runOnUiThread(() -> { + updateDisplayedData(); // Refresh current list to show new stars/history + showToast(getString(R.string.import_success)); + }); + } catch (IOException e) { + Log.e(LOG_TAG, "Import failed", e); + + // Error: Switch back to UI thread to show toast + runOnUiThread(() -> showToast(getString(R.string.import_failed))); + } + }).start(); + } + + // 3. Update the existing startUserDBImport method + public void startUserDBImport() { + // Launches the system file picker to select a SQLite database file + // We accept any file type or specifically application/x-sqlite3 if supported + importDatabaseLauncher.launch(new String[]{"application/x-sqlite3", "application/octet-stream", "*/*"}); + } + /** * Shows a short toast with text=message * @param message From cfa1fb8ebc18fd92ccc61f0cdabaa4cdd259bab1 Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:41:52 +0200 Subject: [PATCH 4/7] - labels for new import/export buttons - success and error messages for import and export --- app/src/main/res/values/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 043afb9..1c9c16a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,12 @@ Teachers Centers Retreats + Export + Import + Database exported successfully! 👍 + Export failed. Please try again. 😖 + Database imported successfully! 👍 + Import failed. Please try again. 😖 Retreat Search Play Talk Download Talk From 619de4ed7fac4fdcde662f81ed9cc6bdb48cf905 Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:32:10 +0200 Subject: [PATCH 5/7] - tweaked import / export buttons in navigation drawer --- .../java/org/dharmaseed/android/NavigationActivity.java | 4 ++-- app/src/main/res/drawable/ic_history_db.xml | 6 +++--- app/src/main/res/drawable/ic_star_db.xml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/dharmaseed/android/NavigationActivity.java b/app/src/main/java/org/dharmaseed/android/NavigationActivity.java index a64ede2..8be85f3 100644 --- a/app/src/main/java/org/dharmaseed/android/NavigationActivity.java +++ b/app/src/main/java/org/dharmaseed/android/NavigationActivity.java @@ -313,10 +313,10 @@ private void initNavigationDrawer(Toolbar toolbar) { private void addDBIcons(MenuItem item, String baseText) { if (item == null) return; - SpannableStringBuilder sb = new SpannableStringBuilder(baseText + " & "); + SpannableStringBuilder sb = new SpannableStringBuilder(baseText + " and "); int iconSize = (int) (headerPrimary.getTextSize() * 0.9); addIconToSpan(sb, R.drawable.ic_history_db, baseText.length() + 1, iconSize); - addIconToSpan(sb, R.drawable.ic_star_db, baseText.length() + 3, iconSize); + addIconToSpan(sb, R.drawable.ic_star_db, 99, iconSize); item.setTitle(sb); } diff --git a/app/src/main/res/drawable/ic_history_db.xml b/app/src/main/res/drawable/ic_history_db.xml index dcff018..8121bcd 100644 --- a/app/src/main/res/drawable/ic_history_db.xml +++ b/app/src/main/res/drawable/ic_history_db.xml @@ -4,9 +4,9 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_star_db.xml b/app/src/main/res/drawable/ic_star_db.xml index e8fa95c..2f05341 100644 --- a/app/src/main/res/drawable/ic_star_db.xml +++ b/app/src/main/res/drawable/ic_star_db.xml @@ -4,8 +4,8 @@ android:viewportWidth="24" android:viewportHeight="24"> \ No newline at end of file From 0ef205df5253b51993006b8cf0f5156829971f4f Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Sun, 26 Apr 2026 11:48:27 +0200 Subject: [PATCH 6/7] redirect www.dharmaseed.org links to the app, too (not just dharmaseed.org) --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce43f63..7fda904 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,6 +48,7 @@ + @@ -71,6 +72,7 @@ + From a3fdbb0d62592b196b909cf7e15b3fd682f56f53 Mon Sep 17 00:00:00 2001 From: intermarc <144038206+intermarc@users.noreply.github.com> Date: Sun, 10 May 2026 19:23:15 +0200 Subject: [PATCH 7/7] removed unused closeAfterCopy argument to DBManager::copyStreams --- .../java/org/dharmaseed/android/DBManager.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/dharmaseed/android/DBManager.java b/app/src/main/java/org/dharmaseed/android/DBManager.java index 67bab12..6f8faec 100644 --- a/app/src/main/java/org/dharmaseed/android/DBManager.java +++ b/app/src/main/java/org/dharmaseed/android/DBManager.java @@ -91,21 +91,18 @@ protected void copyAssetDB(File destFile) throws IOException { InputStream dbIn = context.getAssets().open(DB_NAME); destFile.getParentFile().mkdirs(); OutputStream dbOut = new FileOutputStream(destFile); - copyStreams(dbIn, dbOut, true); + copyStreams(dbIn, dbOut); } - private void copyStreams(InputStream src, OutputStream dest, boolean closeAfterCopy) throws IOException { + private void copyStreams(InputStream src, OutputStream dest) throws IOException { byte[] buf = new byte[1024]; int len; while ((len = src.read(buf)) > 0) { dest.write(buf, 0, len); } dest.flush(); - - if (closeAfterCopy) { - src.close(); - dest.close(); - } + src.close(); + dest.close(); } public static synchronized DBManager getInstance(Context context) { @@ -455,8 +452,7 @@ public void exportUserTablesToUri(Uri uri) throws IOException { // 4. Write DB file to SAF Uri copyStreams( new FileInputStream(tempFile), - context.getContentResolver().openOutputStream(uri), - true + context.getContentResolver().openOutputStream(uri) ); tempFile.delete(); @@ -471,7 +467,7 @@ public void importUserTablesFromUri(Uri uri) throws IOException { // 1. Copy URI content to a temporary file using the existing copyStreams utility InputStream is = context.getContentResolver().openInputStream(uri); if (is == null) throw new IOException("Could not open input stream from URI: " + uri); - copyStreams(is, new FileOutputStream(tempFile), true); + copyStreams(is, new FileOutputStream(tempFile)); // 2. Attach the temporary database db.execSQL("ATTACH DATABASE '" + tempFile.getAbsolutePath() + "' AS import_db");