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.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao 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 import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
@ -31,7 +31,7 @@ class DBOpenHelper(
CategoryDao.Table.onCreate(db) CategoryDao.Table.onCreate(db)
BookmarksTable.onCreate(db) BookmarksTable.onCreate(db)
BookmarkItemsTable.onCreate(db) BookmarkItemsTable.onCreate(db)
RecentSearchesDao.Table.onCreate(db) RecentSearchesTable.onCreate(db)
RecentLanguagesDao.Table.onCreate(db) RecentLanguagesDao.Table.onCreate(db)
} }
@ -39,7 +39,7 @@ class DBOpenHelper(
CategoryDao.Table.onUpdate(db, from, to) CategoryDao.Table.onUpdate(db, from, to)
BookmarksTable.onUpdate(db, from, to) BookmarksTable.onUpdate(db, from, to)
BookmarkItemsTable.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) RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE) deleteTable(db, CONTRIBUTIONS_TABLE)
deleteTable(db, BOOKMARKS_LOCATIONS) deleteTable(db, BOOKMARKS_LOCATIONS)

View file

@ -8,9 +8,9 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.di.CommonsDaggerContentProvider 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.RecentSearchesTable.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.TABLE_NAME
/** /**
* This class contains functions for executing queries for * 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 = fun Cursor.getString(name: String): String =
getString(getColumnIndex(name)) 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 * Converts string to List
* @param listString comma separated single string from of list items * @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.models.RecentSearch
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI 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.RecentSearchesContentProvider.Companion.uriForId
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_NAME import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.CREATE_TABLE_STATEMENT import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.DROP_TABLE_STATEMENT import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onCreate import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onCreate
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onDelete import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onDelete
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onUpdate import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onUpdate
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull