From 2b4021e20a767038ce9c17e4716b28ea6c45583f Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Sat, 12 Jul 2025 13:07:14 -0500 Subject: [PATCH] Convert RecentSearchesDao to kotlin --- .../fr/free/nrw/commons/data/DBOpenHelper.kt | 6 +- .../RecentSearchesContentProvider.kt | 6 +- .../recentsearches/RecentSearchesDao.java | 275 ------------------ .../recentsearches/RecentSearchesDao.kt | 180 ++++++++++++ .../recentsearches/RecentSearchesTable.kt | 71 +++++ .../free/nrw/commons/utils/DatabaseUtils.kt | 8 + .../recentsearches/RecentSearchesDaoTest.kt | 18 +- 7 files changed, 274 insertions(+), 290 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt index 7bbe5de06..55ddec5bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -7,7 +7,7 @@ import android.database.sqlite.SQLiteOpenHelper import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.category.CategoryDao -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao @@ -31,7 +31,7 @@ class DBOpenHelper( CategoryDao.Table.onCreate(db) BookmarksTable.onCreate(db) BookmarkItemsTable.onCreate(db) - RecentSearchesDao.Table.onCreate(db) + RecentSearchesTable.onCreate(db) RecentLanguagesDao.Table.onCreate(db) } @@ -39,7 +39,7 @@ class DBOpenHelper( CategoryDao.Table.onUpdate(db, from, to) BookmarksTable.onUpdate(db, from, to) BookmarkItemsTable.onUpdate(db, from, to) - RecentSearchesDao.Table.onUpdate(db, from, to) + RecentSearchesTable.onUpdate(db, from, to) RecentLanguagesDao.Table.onUpdate(db, from, to) deleteTable(db, CONTRIBUTIONS_TABLE) deleteTable(db, BOOKMARKS_LOCATIONS) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt index f30636db7..21f7a1a22 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt @@ -8,9 +8,9 @@ import android.net.Uri import androidx.core.net.toUri import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.di.CommonsDaggerContentProvider -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.TABLE_NAME /** * This class contains functions for executing queries for diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java deleted file mode 100644 index cee8a25ae..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java +++ /dev/null @@ -1,275 +0,0 @@ -package fr.free.nrw.commons.explore.recentsearches; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.explore.models.RecentSearch; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - -import timber.log.Timber; - -/** - * This class doesn't execute queries in database directly instead it contains the logic behind - * inserting, deleting, searching data from recent searches database. - **/ -public class RecentSearchesDao { - - private final Provider clientProvider; - - @Inject - public RecentSearchesDao(@Named("recentsearch") Provider clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * This method is called on click of media/ categories for storing them in recent searches - * @param recentSearch a recent searches object that is to be added in SqLite DB - */ - public void save(RecentSearch recentSearch) { - ContentProviderClient db = clientProvider.get(); - try { - if (recentSearch.getContentUri() == null) { - recentSearch.setContentUri(db.insert(RecentSearchesContentProvider.BASE_URI, toContentValues(recentSearch))); - } else { - db.update(recentSearch.getContentUri(), toContentValues(recentSearch), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * This method is called on confirmation of delete recent searches. - * It deletes all recent searches from the database - */ - public void deleteAll() { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - RecentSearchesContentProvider.BASE_URI, - Table.ALL_FIELDS, - null, - new String[]{}, - Table.COLUMN_LAST_USED + " DESC" - ); - while (cursor != null && cursor.moveToNext()) { - try { - RecentSearch recentSearch = find(fromCursor(cursor).getQuery()); - if (recentSearch.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - Timber.d("QUERY_NAME %s - delete tried", recentSearch.getContentUri()); - db.delete(recentSearch.getContentUri(), null, null); - Timber.d("QUERY_NAME %s - query deleted", recentSearch.getQuery()); - } - } catch (RemoteException e) { - Timber.e(e, "query deleted"); - throw new RuntimeException(e); - } finally { - db.release(); - } - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - /** - * Deletes a recent search from the database - */ - public void delete(RecentSearch recentSearch) { - - ContentProviderClient db = clientProvider.get(); - try { - if (recentSearch.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - db.delete(recentSearch.getContentUri(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - - /** - * Find persisted search query in database, based on its name. - * @param name Search query Ex- "butterfly" - * @return recently searched query from database, or null if not found - */ - @Nullable - public RecentSearch find(String name) { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - RecentSearchesContentProvider.BASE_URI, - Table.ALL_FIELDS, - Table.COLUMN_NAME + "=?", - new String[]{name}, - null); - if (cursor != null && cursor.moveToFirst()) { - return fromCursor(cursor); - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return null; - } - - /** - * Retrieve recently-searched queries, ordered by descending date. - * @return a list containing recent searches - */ - @NonNull - public List recentSearches(int limit) { - List items = new ArrayList<>(); - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( RecentSearchesContentProvider.BASE_URI, Table.ALL_FIELDS, - null, new String[]{}, Table.COLUMN_LAST_USED + " DESC"); - // fixme add a limit on the original query instead of falling out of the loop? - while (cursor != null && cursor.moveToNext() && cursor.getPosition() < limit) { - items.add(fromCursor(cursor).getQuery()); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return items; - } - - - /** - * It creates an Recent Searches object from data stored in the SQLite DB by using cursor - * @param cursor - * @return RecentSearch object - */ - @NonNull - @SuppressLint("Range") - RecentSearch fromCursor(Cursor cursor) { - // Hardcoding column positions! - return new RecentSearch( - RecentSearchesContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), - new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))) - ); - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - private ContentValues toContentValues(RecentSearch recentSearch) { - ContentValues cv = new ContentValues(); - cv.put(RecentSearchesDao.Table.COLUMN_NAME, recentSearch.getQuery()); - cv.put(RecentSearchesDao.Table.COLUMN_LAST_USED, recentSearch.getLastSearched().getTime()); - return cv; - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - public static class Table { - public static final String TABLE_NAME = "recent_searches"; - public static final String COLUMN_ID = "_id"; - static final String COLUMN_NAME = "name"; - static final String COLUMN_LAST_USED = "last_used"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_NAME, - COLUMN_LAST_USED, - }; - - static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - - static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_ID + " INTEGER PRIMARY KEY," - + COLUMN_NAME + " STRING," - + COLUMN_LAST_USED + " INTEGER" - + ");"; - - /** - * This method creates a RecentSearchesTable in SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * This method deletes RecentSearchesTable from SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onDelete(SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - /** - * This method is called on migrating from a older version to a newer version - * @param db SQLiteDatabase - * @param from Version from which we are migrating - * @param to Version to which we are migrating - */ - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 6) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 6) { - // table added in version 7 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - if (from == 7) { - from++; - onUpdate(db, from, to); - return; - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt new file mode 100644 index 000000000..e1d0740de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt @@ -0,0 +1,180 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.annotation.SuppressLint +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.os.RemoteException +import androidx.core.content.contentValuesOf +import fr.free.nrw.commons.explore.models.RecentSearch +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +import fr.free.nrw.commons.utils.getInt +import fr.free.nrw.commons.utils.getLong +import fr.free.nrw.commons.utils.getString +import java.util.Date +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider + +/** + * This class doesn't execute queries in database directly instead it contains the logic behind + * inserting, deleting, searching data from recent searches database. + */ +class RecentSearchesDao @Inject constructor( + @param:Named("recentsearch") private val clientProvider: Provider +) { + /** + * This method is called on click of media/ categories for storing them in recent searches + * @param recentSearch a recent searches object that is to be added in SqLite DB + */ + fun save(recentSearch: RecentSearch) { + val db = clientProvider.get() + try { + val contentValues = toContentValues(recentSearch) + if (recentSearch.contentUri == null) { + recentSearch.contentUri = db.insert(BASE_URI, contentValues) + } else { + db.update(recentSearch.contentUri!!, contentValues, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * This method is called on confirmation of delete recent searches. + * It deletes all recent searches from the database + */ + fun deleteAll() { + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, + ALL_FIELDS, + null, + arrayOf(), + "$COLUMN_LAST_USED DESC" + ) + while (cursor != null && cursor.moveToNext()) { + try { + val recentSearch = find(fromCursor(cursor).query) + if (recentSearch!!.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(recentSearch.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + } + } + + /** + * Deletes a recent search from the database + */ + fun delete(recentSearch: RecentSearch) { + val db = clientProvider.get() + try { + if (recentSearch.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(recentSearch.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + + /** + * Find persisted search query in database, based on its name. + * @param name Search query Ex- "butterfly" + * @return recently searched query from database, or null if not found + */ + fun find(name: String): RecentSearch? { + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, + ALL_FIELDS, + "$COLUMN_NAME=?", + arrayOf(name), + null + ) + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor) + } + } catch (e: RemoteException) { + // This feels lazy, but to hell with checked exceptions. :) + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return null + } + + /** + * Retrieve recently-searched queries, ordered by descending date. + * @return a list containing recent searches + */ + fun recentSearches(limit: Int): List { + val items: MutableList = mutableListOf() + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, ALL_FIELDS, + null, arrayOf(), "$COLUMN_LAST_USED DESC" + ) + // fixme add a limit on the original query instead of falling out of the loop? + while (cursor != null && cursor.moveToNext() && cursor.position < limit) { + items.add(fromCursor(cursor).query) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return items + } + + /** + * It creates an Recent Searches object from data stored in the SQLite DB by using cursor + * @param cursor + * @return RecentSearch object + */ + fun fromCursor(cursor: Cursor): RecentSearch = RecentSearch( + uriForId(cursor.getInt(COLUMN_ID)), + cursor.getString(COLUMN_NAME), + Date(cursor.getLong(COLUMN_LAST_USED)) + ) + + /** + * This class contains the database table architechture for recent searches, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + private fun toContentValues(recentSearch: RecentSearch): ContentValues = contentValuesOf( + COLUMN_NAME to recentSearch.query, + COLUMN_LAST_USED to recentSearch.lastSearched.time + ) +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt new file mode 100644 index 000000000..e32fc9fa4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.database.sqlite.SQLiteDatabase + +/** + * This class contains the database table architechture for recent searches, It also contains + * queries and logic necessary to the create, update, delete this table. + */ +object RecentSearchesTable { + const val TABLE_NAME: String = "recent_searches" + const val COLUMN_ID: String = "_id" + const val COLUMN_NAME: String = "name" + const val COLUMN_LAST_USED: String = "last_used" + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + @JvmField + val ALL_FIELDS = arrayOf( + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + ) + + const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME" + + const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY,$COLUMN_NAME STRING,$COLUMN_LAST_USED INTEGER);") + + /** + * This method creates a RecentSearchesTable in SQLiteDatabase + * + * @param db SQLiteDatabase + */ + fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT) + + /** + * This method deletes RecentSearchesTable from SQLiteDatabase + * + * @param db SQLiteDatabase + */ + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + /** + * This method is called on migrating from a older version to a newer version + * + * @param db SQLiteDatabase + * @param from Version from which we are migrating + * @param to Version to which we are migrating + */ + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + if (from < 6) { + // doesn't exist yet + onUpdate(db, from + 1, to) + return + } + if (from == 6) { + // table added in version 7 + onCreate(db) + onUpdate(db, from + 1, to) + return + } + if (from == 7) { + onUpdate(db, from + 1, to) + return + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt index 69560279b..1fd99bcee 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt @@ -10,6 +10,14 @@ fun Cursor.getStringArray(name: String): List = fun Cursor.getString(name: String): String = getString(getColumnIndex(name)) +@SuppressLint("Range") +fun Cursor.getInt(name: String): Int = + getInt(getColumnIndex(name)) + +@SuppressLint("Range") +fun Cursor.getLong(name: String): Long = + getLong(getColumnIndex(name)) + /** * Converts string to List * @param listString comma separated single string from of list items diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt index c772f796e..5e128d4ee 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt @@ -20,15 +20,15 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.explore.models.RecentSearch import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_NAME -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onCreate -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onDelete -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onUpdate +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.CREATE_TABLE_STATEMENT +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.DROP_TABLE_STATEMENT +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onCreate +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onDelete +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onUpdate import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull