Convert RecentSearchesDao to kotlin

This commit is contained in:
Paul Hawke 2025-07-12 13:07:14 -05:00
parent b65f76853a
commit 2b4021e20a
7 changed files with 274 additions and 290 deletions

View file

@ -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)

View file

@ -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

View file

@ -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<ContentProviderClient> clientProvider;
@Inject
public RecentSearchesDao(@Named("recentsearch") Provider<ContentProviderClient> 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<String> recentSearches(int limit) {
List<String> 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;
}
}
}
}

View file

@ -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<ContentProviderClient>
) {
/**
* 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<String> {
val items: MutableList<String> = 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
)
}

View file

@ -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
}
}
}

View file

@ -10,6 +10,14 @@ fun Cursor.getStringArray(name: String): List<String> =
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

View file

@ -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