Refactor: Migrate bookmark location logic to Kotlin

This commit migrates the bookmark location logic to Kotlin, enhancing code maintainability and readability.

-   Removes the `BookmarkLocationsContentProvider`, `BookmarkLocationsController`, and `BookmarkLocationsDao` Java classes.
-   Creates `BookmarkLocationsDao.kt` and `BookmarkLocationsContentProvider.kt` in Kotlin.
-   Migrates the logic from `BookmarkLocationsFragment.java` to `BookmarkLocationsFragment.kt`.
-   Updates test files to reflect these changes.
-   Addresses associated code review comments.
This commit is contained in:
Saifuddin 2025-01-16 18:00:39 +05:30
parent f191dcf68f
commit f32c59034d
7 changed files with 423 additions and 445 deletions

View file

@ -1,119 +1,126 @@
package fr.free.nrw.commons.bookmarks.locations; package fr.free.nrw.commons.bookmarks.locations
import android.content.ContentValues; // We can get uri using java.Net.Uri, but android implementation is faster
import android.database.Cursor; // (but it's forgiving with handling exceptions though)
import android.database.sqlite.SQLiteDatabase; import android.content.ContentValues
import android.database.sqlite.SQLiteQueryBuilder; import android.database.Cursor
// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though) import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri; import android.net.Uri
import android.text.TextUtils; import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_NAME
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.TABLE_NAME
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import timber.log.Timber
import javax.inject.Inject
import androidx.annotation.NonNull;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_NAME;
import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.TABLE_NAME;
/** /**
* Handles private storage for Bookmark locations * Handles private storage for Bookmark locations
*/ */
public class BookmarkLocationsContentProvider extends CommonsDaggerContentProvider { class BookmarkLocationsContentProvider : CommonsDaggerContentProvider() {
private static final String BASE_PATH = "bookmarksLocations"; companion object {
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY + "/" + BASE_PATH); private const val BASE_PATH = "bookmarksLocations"
val BASE_URI: Uri =
Uri.parse("content://${BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY}/$BASE_PATH")
/** /**
* Append bookmark locations name to the base uri * Append bookmark locations name to the base URI.
*/ */
public static Uri uriForName(String name) { fun uriForName(name: String): Uri {
return Uri.parse(BASE_URI.toString() + "/" + name); return Uri.parse("$BASE_URI/$name")
}
@Inject DBOpenHelper dbOpenHelper;
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* Queries the SQLite database for the bookmark locations
* @param uri : contains the uri for bookmark locations
* @param projection
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark locations
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_NAME + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
} }
getContext().getContentResolver().notifyChange(uri, null); }
return rowsUpdated;
@Inject
lateinit var dbOpenHelper: DBOpenHelper
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the bookmark locations.
*/
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val db = dbOpenHelper.readableDatabase
val cursor = queryBuilder.query(
db,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
cursor.setNotificationUri(context?.contentResolver, uri)
return cursor
} }
/** /**
* Handles the insertion of new bookmark locations record to local SQLite Database * Handles the update query of local SQLite database.
*/ */
@SuppressWarnings("ConstantConditions") override fun update(
@Override uri: Uri,
public Uri insert(@NonNull Uri uri, ContentValues contentValues) { contentValues: ContentValues?,
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); selection: String?,
long id = sqlDB.insert(BookmarkLocationsDao.Table.TABLE_NAME, null, contentValues); selectionArgs: Array<String>?
getContext().getContentResolver().notifyChange(uri, null); ): Int {
return Uri.parse(BASE_URI + "/" + id); val db = dbOpenHelper.writableDatabase
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment?.toIntOrNull()
?: throw IllegalArgumentException("Invalid ID in URI")
rowsUpdated = db.update(
TABLE_NAME,
contentValues,
"$COLUMN_NAME = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
context?.contentResolver?.notifyChange(uri, null)
return rowsUpdated
} }
@SuppressWarnings("ConstantConditions") /**
@Override * Handles the insertion of a new bookmark locations record to the local SQLite database.
public int delete(@NonNull Uri uri, String s, String[] strings) { */
int rows; override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); val db = dbOpenHelper.writableDatabase
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment()); val id = db.insert(TABLE_NAME, null, contentValues)
rows = db.delete(TABLE_NAME, context?.contentResolver?.notifyChange(uri, null)
"location_name = ?", return Uri.parse("$BASE_URI/$id")
new String[]{uri.getLastPathSegment()} }
);
getContext().getContentResolver().notifyChange(uri, null); /**
return rows; * Handles the deletion of bookmark locations from the local SQLite database.
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbOpenHelper.readableDatabase
Timber.d("Deleting bookmark name %s", uri.lastPathSegment)
val rows = db.delete(
TABLE_NAME,
"location_name = ?",
arrayOf(uri.lastPathSegment)
)
context?.contentResolver?.notifyChange(uri, null)
return rows
} }
} }

View file

@ -1,26 +1,17 @@
package fr.free.nrw.commons.bookmarks.locations; package fr.free.nrw.commons.bookmarks.locations
import java.util.List; import fr.free.nrw.commons.nearby.Place
import javax.inject.Inject
import javax.inject.Inject; import javax.inject.Singleton
import javax.inject.Singleton;
import fr.free.nrw.commons.nearby.Place;
@Singleton @Singleton
public class BookmarkLocationsController { class BookmarkLocationsController @Inject constructor(
private val bookmarkLocationDao: BookmarkLocationsDao
@Inject ) {
BookmarkLocationsDao bookmarkLocationDao;
@Inject
public BookmarkLocationsController() {}
/** /**
* Load from DB the bookmarked locations * Load bookmarked locations from the database.
* @return a list of Place objects. * @return a list of Place objects.
*/ */
public List<Place> loadFavoritesLocations() { fun loadFavoritesLocations(): List<Place> = bookmarkLocationDao.getAllBookmarksLocations()
return bookmarkLocationDao.getAllBookmarksLocations();
}
} }

View file

@ -1,313 +1,286 @@
package fr.free.nrw.commons.bookmarks.locations; package fr.free.nrw.commons.bookmarks.locations
import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.RemoteException;
import androidx.annotation.NonNull; import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.os.RemoteException
import androidx.annotation.NonNull
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Label
import fr.free.nrw.commons.nearby.NearbyController
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.Sitelinks
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import fr.free.nrw.commons.nearby.NearbyController;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; class BookmarkLocationsDao @Inject constructor(
import javax.inject.Named; @Named("bookmarksLocation") private val clientProvider: Provider<ContentProviderClient>
import javax.inject.Provider; ) {
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Label;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.Sitelinks;
import timber.log.Timber;
import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider.BASE_URI;
public class BookmarkLocationsDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public BookmarkLocationsDao(@Named("bookmarksLocation") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/** /**
* Find all persisted locations bookmarks on database * Find all persisted location bookmarks in the database
*
* @return list of Place * @return list of Place
*/ */
@NonNull fun getAllBookmarksLocations(): List<Place> {
public List<Place> getAllBookmarksLocations() { val items = mutableListOf<Place>()
List<Place> items = new ArrayList<>(); var cursor: Cursor? = null
Cursor cursor = null; val db = clientProvider.get()
ContentProviderClient db = clientProvider.get();
try { try {
cursor = db.query( cursor = db.query(
BookmarkLocationsContentProvider.BASE_URI, BookmarkLocationsContentProvider.BASE_URI,
Table.ALL_FIELDS, Table.ALL_FIELDS,
null, null,
new String[]{}, emptyArray(),
null); null
while (cursor != null && cursor.moveToNext()) { )
items.add(fromCursor(cursor)); cursor?.let {
while (it.moveToNext()) {
items.add(fromCursor(it))
}
} }
} catch (RemoteException e) { } catch (e: RemoteException) {
throw new RuntimeException(e); throw RuntimeException(e)
} finally { } finally {
if (cursor != null) { cursor?.close()
cursor.close(); db.release()
}
db.release();
} }
return items; return items
} }
/** /**
* Look for a place in bookmarks table in order to insert or delete it * Look for a place in bookmarks table in order to insert or delete it
*
* @param bookmarkLocation : Place object * @param bookmarkLocation : Place object
* @return is Place now fav ? * @return boolean : is Place now fav ?
*/ */
public boolean updateBookmarkLocation(Place bookmarkLocation) { fun updateBookmarkLocation(bookmarkLocation: Place): Boolean {
boolean bookmarkExists = findBookmarkLocation(bookmarkLocation); val bookmarkExists = findBookmarkLocation(bookmarkLocation)
if (bookmarkExists) { if (bookmarkExists) {
deleteBookmarkLocation(bookmarkLocation); deleteBookmarkLocation(bookmarkLocation)
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false); NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false)
} else { } else {
addBookmarkLocation(bookmarkLocation); addBookmarkLocation(bookmarkLocation)
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true); NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true)
} }
return !bookmarkExists; return !bookmarkExists
} }
/** /**
* Add a Place to bookmarks table * Add a Place to bookmarks table
*
* @param bookmarkLocation : Place to add * @param bookmarkLocation : Place to add
*/ */
private void addBookmarkLocation(Place bookmarkLocation) { private fun addBookmarkLocation(bookmarkLocation: Place) {
ContentProviderClient db = clientProvider.get(); val db = clientProvider.get()
try { try {
db.insert(BASE_URI, toContentValues(bookmarkLocation)); db.insert(BookmarkLocationsContentProvider.BASE_URI, toContentValues(bookmarkLocation))
} catch (RemoteException e) { } catch (e: RemoteException) {
throw new RuntimeException(e); throw RuntimeException(e)
} finally { } finally {
db.release(); db.release()
} }
} }
/** /**
* Delete a Place from bookmarks table * Delete a Place from bookmarks table
*
* @param bookmarkLocation : Place to delete * @param bookmarkLocation : Place to delete
*/ */
private void deleteBookmarkLocation(Place bookmarkLocation) { private fun deleteBookmarkLocation(bookmarkLocation: Place) {
ContentProviderClient db = clientProvider.get(); val db = clientProvider.get()
try { try {
db.delete(BookmarkLocationsContentProvider.uriForName(bookmarkLocation.name), null, null); db.delete(
} catch (RemoteException e) { BookmarkLocationsContentProvider.uriForName(bookmarkLocation.name),
throw new RuntimeException(e); null,
null
)
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally { } finally {
db.release(); db.release()
} }
} }
/** /**
* Find a Place from database based on its name * Find a Place from database based on its name
*
* @param bookmarkLocation : Place to find * @param bookmarkLocation : Place to find
* @return boolean : is Place in database ? * @return boolean : is Place in database ?
*/ */
public boolean findBookmarkLocation(Place bookmarkLocation) { fun findBookmarkLocation(bookmarkLocation: Place): Boolean {
Cursor cursor = null; var cursor: Cursor? = null
ContentProviderClient db = clientProvider.get(); val db = clientProvider.get()
try { try {
cursor = db.query( cursor = db.query(
BookmarkLocationsContentProvider.BASE_URI, BookmarkLocationsContentProvider.BASE_URI,
Table.ALL_FIELDS, Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?", "${Table.COLUMN_NAME}=?",
new String[]{bookmarkLocation.name}, arrayOf(bookmarkLocation.name),
null); null
if (cursor != null && cursor.moveToFirst()) { )
return true; return cursor?.moveToFirst() == true
} } catch (e: RemoteException) {
} catch (RemoteException e) { throw RuntimeException(e)
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally { } finally {
if (cursor != null) { cursor?.close()
cursor.close(); db.release()
}
db.release();
} }
return false;
} }
@SuppressLint("Range") @SuppressLint("Range")
@NonNull @NonNull
Place fromCursor(final Cursor cursor) { fun fromCursor(cursor: Cursor): Place {
final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), val location = LatLng(
cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F); cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)),
cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)),
1F
)
final Sitelinks.Builder builder = new Sitelinks.Builder(); val builder = Sitelinks.Builder().apply {
builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK))); setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK)))
builder.setWikidataLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIDATA_LINK))); setWikidataLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIDATA_LINK)))
builder.setCommonsLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_COMMONS_LINK))); setCommonsLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_COMMONS_LINK)))
}
return new Place( return Place(
cursor.getString(cursor.getColumnIndex(Table.COLUMN_LANGUAGE)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_LANGUAGE)),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
Label.fromText((cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT)))), Label.fromText(cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT))),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
location, location,
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)),
builder.build(), builder.build(),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)),
Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(Table.COLUMN_EXISTS))) cursor.getString(cursor.getColumnIndex(Table.COLUMN_EXISTS))?.toBoolean() ?: false
); )
} }
private ContentValues toContentValues(Place bookmarkLocation) { private fun toContentValues(bookmarkLocation: Place): ContentValues {
ContentValues cv = new ContentValues(); return ContentValues().apply {
cv.put(BookmarkLocationsDao.Table.COLUMN_NAME, bookmarkLocation.getName()); put(Table.COLUMN_NAME, bookmarkLocation.name)
cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage()); put(Table.COLUMN_LANGUAGE, bookmarkLocation.language)
cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription()); put(Table.COLUMN_DESCRIPTION, bookmarkLocation.longDescription)
cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory()); put(Table.COLUMN_CATEGORY, bookmarkLocation.category)
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getText() : ""); put(Table.COLUMN_LABEL_TEXT, bookmarkLocation.label?.text ?: "")
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getIcon() : null); put(Table.COLUMN_LABEL_ICON, bookmarkLocation.label?.icon)
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.getWikipediaLink().toString()); put(Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.wikipediaLink.toString())
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.getWikidataLink().toString()); put(Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.wikidataLink.toString())
cv.put(BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.getCommonsLink().toString()); put(Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.commonsLink.toString())
cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude()); put(Table.COLUMN_LAT, bookmarkLocation.location.latitude)
cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude()); put(Table.COLUMN_LONG, bookmarkLocation.location.longitude)
cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic); put(Table.COLUMN_PIC, bookmarkLocation.pic)
cv.put(BookmarkLocationsDao.Table.COLUMN_EXISTS, bookmarkLocation.exists.toString()); put(Table.COLUMN_EXISTS, bookmarkLocation.exists.toString())
return cv; }
} }
public static class Table { object Table {
public static final String TABLE_NAME = "bookmarksLocations"; const val TABLE_NAME = "bookmarksLocations"
static final String COLUMN_NAME = "location_name"; const val COLUMN_NAME = "location_name"
static final String COLUMN_LANGUAGE = "location_language"; const val COLUMN_LANGUAGE = "location_language"
static final String COLUMN_DESCRIPTION = "location_description"; const val COLUMN_DESCRIPTION = "location_description"
static final String COLUMN_LAT = "location_lat"; const val COLUMN_LAT = "location_lat"
static final String COLUMN_LONG = "location_long"; const val COLUMN_LONG = "location_long"
static final String COLUMN_CATEGORY = "location_category"; const val COLUMN_CATEGORY = "location_category"
static final String COLUMN_LABEL_TEXT = "location_label_text"; const val COLUMN_LABEL_TEXT = "location_label_text"
static final String COLUMN_LABEL_ICON = "location_label_icon"; const val COLUMN_LABEL_ICON = "location_label_icon"
static final String COLUMN_IMAGE_URL = "location_image_url"; const val COLUMN_IMAGE_URL = "location_image_url"
static final String COLUMN_WIKIPEDIA_LINK = "location_wikipedia_link"; const val COLUMN_WIKIPEDIA_LINK = "location_wikipedia_link"
static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link"; const val COLUMN_WIKIDATA_LINK = "location_wikidata_link"
static final String COLUMN_COMMONS_LINK = "location_commons_link"; const val COLUMN_COMMONS_LINK = "location_commons_link"
static final String COLUMN_PIC = "location_pic"; const val COLUMN_PIC = "location_pic"
static final String COLUMN_EXISTS = "location_exists"; const val COLUMN_EXISTS = "location_exists"
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = { val ALL_FIELDS = arrayOf(
COLUMN_NAME, COLUMN_NAME, COLUMN_LANGUAGE, COLUMN_DESCRIPTION, COLUMN_CATEGORY, COLUMN_LABEL_TEXT, COLUMN_LABEL_ICON,
COLUMN_LANGUAGE, COLUMN_LAT, COLUMN_LONG, COLUMN_IMAGE_URL, COLUMN_WIKIPEDIA_LINK, COLUMN_WIKIDATA_LINK, COLUMN_COMMONS_LINK,
COLUMN_DESCRIPTION, COLUMN_PIC, COLUMN_EXISTS
COLUMN_CATEGORY, )
COLUMN_LABEL_TEXT,
COLUMN_LABEL_ICON,
COLUMN_LAT,
COLUMN_LONG,
COLUMN_IMAGE_URL,
COLUMN_WIKIPEDIA_LINK,
COLUMN_WIKIDATA_LINK,
COLUMN_COMMONS_LINK,
COLUMN_PIC,
COLUMN_EXISTS,
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" const val CREATE_TABLE_STATEMENT = """
+ COLUMN_NAME + " STRING PRIMARY KEY," CREATE TABLE $TABLE_NAME (
+ COLUMN_LANGUAGE + " STRING," $COLUMN_NAME STRING PRIMARY KEY,
+ COLUMN_DESCRIPTION + " STRING," $COLUMN_LANGUAGE STRING,
+ COLUMN_CATEGORY + " STRING," $COLUMN_DESCRIPTION STRING,
+ COLUMN_LABEL_TEXT + " STRING," $COLUMN_CATEGORY STRING,
+ COLUMN_LABEL_ICON + " INTEGER," $COLUMN_LABEL_TEXT STRING,
+ COLUMN_LAT + " DOUBLE," $COLUMN_LABEL_ICON INTEGER,
+ COLUMN_LONG + " DOUBLE," $COLUMN_LAT DOUBLE,
+ COLUMN_IMAGE_URL + " STRING," $COLUMN_LONG DOUBLE,
+ COLUMN_WIKIPEDIA_LINK + " STRING," $COLUMN_IMAGE_URL STRING,
+ COLUMN_WIKIDATA_LINK + " STRING," $COLUMN_WIKIPEDIA_LINK STRING,
+ COLUMN_COMMONS_LINK + " STRING," $COLUMN_WIKIDATA_LINK STRING,
+ COLUMN_PIC + " STRING," $COLUMN_COMMONS_LINK STRING,
+ COLUMN_EXISTS + " STRING" $COLUMN_PIC STRING,
+ ");"; $COLUMN_EXISTS STRING
);
"""
public static void onCreate(SQLiteDatabase db) { fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT); db.execSQL(CREATE_TABLE_STATEMENT)
} }
public static void onDelete(SQLiteDatabase db) { fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT); db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db); onCreate(db)
} }
public static void onUpdate(final SQLiteDatabase db, int from, final int to) { @SuppressLint("SQLiteString")
Timber.d("bookmarksLocations db is updated from:"+from+", to:"+to); fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
Timber.d("bookmarksLocations db is updated from:$from, to:$to")
var currFrom = from
if (from == to) { if (from == to) {
return; return
} }
if (from < 7) { if (from < 7) {
// doesn't exist yet onUpdate(db, ++currFrom, to)
from++; return
onUpdate(db, from, to);
return;
} }
if (from == 7) { if (from == 7) {
// table added in version 8 onCreate(db)
onCreate(db); onUpdate(db, ++currFrom, to)
from++; return
onUpdate(db, from, to);
return;
} }
if (from < 10) { if (from < 10) {
from++; onUpdate(db, ++currFrom, to)
onUpdate(db, from, to); return
return;
} }
if (from == 10) { if (from == 10) {
//This is safe, and can be called clean, as we/I do not remember the appropriate version for this
//We are anyways switching to room, these things won't be necessary then
try { try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;"); db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN location_pic STRING;")
}catch (SQLiteException exception){ } catch (exception: SQLiteException) {
Timber.e(exception);// Timber.e(exception)
} }
return; return
} }
if (from >= 12) { if (from >= 12) {
try { try {
db.execSQL( db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN location_destroyed STRING;")
"ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;"); } catch (exception: SQLiteException) {
} catch (SQLiteException exception) { Timber.e(exception)
Timber.e(exception);
} }
} }
if (from >= 13){ if (from >= 13) {
try { try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;"); db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN location_language STRING;")
} catch (SQLiteException exception){ } catch (exception: SQLiteException) {
Timber.e(exception); Timber.e(exception)
} }
} }
if (from >= 14){ if (from >= 14) {
try { try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;"); db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN location_exists STRING;")
} catch (SQLiteException exception){ } catch (exception: SQLiteException) {
Timber.e(exception); Timber.e(exception)
} }
} }
} }
} }
} }

View file

@ -1,137 +1,145 @@
package fr.free.nrw.commons.bookmarks.locations; package fr.free.nrw.commons.bookmarks.locations
import android.Manifest.permission; import android.Manifest.permission
import android.content.Intent; import android.os.Bundle
import android.os.Bundle; import android.view.LayoutInflater
import android.view.LayoutInflater; import android.view.View
import android.view.View; import android.view.ViewGroup
import android.view.ViewGroup; import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.result.contract.ActivityResultContracts; import androidx.recyclerview.widget.LinearLayoutManager
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import dagger.android.support.DaggerFragment
import androidx.annotation.NonNull; import fr.free.nrw.commons.R
import androidx.annotation.Nullable; import fr.free.nrw.commons.contributions.ContributionController
import androidx.recyclerview.widget.LinearLayoutManager; import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding
import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.filepicker.FilePicker
import fr.free.nrw.commons.R; import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.nearby.fragments.PlaceAdapter
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding; import javax.inject.Inject
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions;
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import kotlin.Unit;
public class BookmarkLocationsFragment extends DaggerFragment {
public FragmentBookmarksLocationsBinding binding; class BookmarkLocationsFragment : DaggerFragment() {
@Inject BookmarkLocationsController controller; private var binding: FragmentBookmarksLocationsBinding? = null
@Inject ContributionController contributionController;
@Inject BookmarkLocationsDao bookmarkLocationDao;
@Inject CommonPlaceClickActions commonPlaceClickActions;
private PlaceAdapter adapter;
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult = @Inject lateinit var controller: BookmarkLocationsController
registerForActivityResult(new StartActivityForResult(), @Inject lateinit var contributionController: ContributionController
result -> { @Inject lateinit var bookmarkLocationDao: BookmarkLocationsDao
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { @Inject lateinit var commonPlaceClickActions: CommonPlaceClickActions
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
});
});
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult = private lateinit var inAppCameraLocationPermissionLauncher:
registerForActivityResult(new StartActivityForResult(), ActivityResultLauncher<Array<String>>
result -> { private lateinit var adapter: PlaceAdapter
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
});
});
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { private val cameraPickLauncherForResult =
@Override registerForActivityResult(StartActivityForResult()) { result ->
public void onActivityResult(Map<String, Boolean> result) { contributionController.handleActivityResultWithCallback(
boolean areAllGranted = true; requireActivity(),
for(final boolean b : result.values()) { object: FilePicker.HandleActivityResult {
areAllGranted = areAllGranted && b; override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
} contributionController.onPictureReturnedFromCamera(
result,
requireActivity(),
callbacks
)
}
}
)
}
if (areAllGranted) { private val galleryPickLauncherForResult =
contributionController.locationPermissionCallback.onLocationPermissionGranted(); registerForActivityResult(StartActivityForResult()) { result ->
} else { contributionController.handleActivityResultWithCallback(
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { requireActivity(),
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); object: FilePicker.HandleActivityResult {
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
contributionController.onPictureReturnedFromGallery(
result,
requireActivity(),
callbacks
)
}
}
)
}
companion object {
fun newInstance(): BookmarkLocationsFragment {
return BookmarkLocationsFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.loadingImagesProgressBar?.visibility = View.VISIBLE
binding?.listView?.layoutManager = LinearLayoutManager(context)
inAppCameraLocationPermissionLauncher =
registerForActivityResult(RequestMultiplePermissions()) { result ->
val areAllGranted = result.values.all { it }
if (areAllGranted) {
contributionController.locationPermissionCallback.onLocationPermissionGranted()
} else { } else {
contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied)); if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
contributionController.handleShowRationaleFlowCameraLocation(
activity,
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
} else {
contributionController.locationPermissionCallback
.onLocationPermissionDenied(
getString(R.string.in_app_camera_location_permission_denied)
)
}
} }
} }
}
});
/** adapter = PlaceAdapter(
* Create an instance of the fragment with the right bundle parameters bookmarkLocationDao,
* @return an instance of the fragment { },
*/ { place, _ ->
public static BookmarkLocationsFragment newInstance() { adapter.remove(place)
return new BookmarkLocationsFragment();
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState
) {
binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.loadingImagesProgressBar.setVisibility(View.VISIBLE);
binding.listView.setLayoutManager(new LinearLayoutManager(getContext()));
adapter = new PlaceAdapter(bookmarkLocationDao,
place -> Unit.INSTANCE,
(place, isBookmarked) -> {
adapter.remove(place);
return Unit.INSTANCE;
}, },
commonPlaceClickActions, commonPlaceClickActions,
inAppCameraLocationPermissionLauncher, inAppCameraLocationPermissionLauncher,
galleryPickLauncherForResult, galleryPickLauncherForResult,
cameraPickLauncherForResult cameraPickLauncherForResult
); )
binding.listView.setAdapter(adapter); binding?.listView?.adapter = adapter
} }
@Override override fun onResume() {
public void onResume() { super.onResume()
super.onResume(); initList()
initList();
} }
/** private fun initList() {
* Initialize the recycler view with bookmarked locations val places = controller.loadFavoritesLocations()
*/ adapter.items = places
private void initList() { binding?.loadingImagesProgressBar?.visibility = View.GONE
List<Place> places = controller.loadFavoritesLocations(); if (places.isEmpty()) {
adapter.setItems(places); binding?.statusMessage?.text = getString(R.string.bookmark_empty)
binding.loadingImagesProgressBar.setVisibility(View.GONE); binding?.statusMessage?.visibility = View.VISIBLE
if (places.size() <= 0) {
binding.statusMessage.setText(R.string.bookmark_empty);
binding.statusMessage.setVisibility(View.VISIBLE);
} else { } else {
binding.statusMessage.setVisibility(View.GONE); binding?.statusMessage?.visibility = View.GONE
} }
} }
@Override override fun onDestroy() {
public void onDestroy() { super.onDestroy()
super.onDestroy(); // Make sure to null out the binding to avoid memory leaks
binding = null; binding = null
} }
} }

View file

@ -18,7 +18,6 @@ import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider.BASE_URI
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_CATEGORY import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_CATEGORY
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_DESCRIPTION import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_DESCRIPTION
@ -149,7 +148,7 @@ class BookMarkLocationDaoTest {
fun getAllLocationBookmarks() { fun getAllLocationBookmarks() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14)) whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14))
var result = testObject.allBookmarksLocations var result = testObject.getAllBookmarksLocations()
assertEquals(14, result.size) assertEquals(14, result.size)
} }
@ -157,19 +156,19 @@ class BookMarkLocationDaoTest {
@Test(expected = RuntimeException::class) @Test(expected = RuntimeException::class)
fun getAllLocationBookmarksTranslatesExceptions() { fun getAllLocationBookmarksTranslatesExceptions() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException(""))
testObject.allBookmarksLocations testObject.getAllBookmarksLocations()
} }
@Test @Test
fun getAllLocationBookmarksReturnsEmptyList_emptyCursor() { fun getAllLocationBookmarksReturnsEmptyList_emptyCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0)) whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0))
assertTrue(testObject.allBookmarksLocations.isEmpty()) assertTrue(testObject.getAllBookmarksLocations().isEmpty())
} }
@Test @Test
fun getAllLocationBookmarksReturnsEmptyList_nullCursor() { fun getAllLocationBookmarksReturnsEmptyList_nullCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null)
assertTrue(testObject.allBookmarksLocations.isEmpty()) assertTrue(testObject.getAllBookmarksLocations().isEmpty())
} }
@Test @Test
@ -178,7 +177,7 @@ class BookMarkLocationDaoTest {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor)
whenever(mockCursor.moveToFirst()).thenReturn(false) whenever(mockCursor.moveToFirst()).thenReturn(false)
testObject.allBookmarksLocations testObject.getAllBookmarksLocations()
verify(mockCursor).close() verify(mockCursor).close()
} }
@ -189,7 +188,7 @@ class BookMarkLocationDaoTest {
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null)
assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark)) assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark))
verify(client).insert(eq(BASE_URI), captor.capture()) verify(client).insert(eq(BookmarkLocationsContentProvider.BASE_URI), captor.capture())
captor.firstValue.let { cv -> captor.firstValue.let { cv ->
assertEquals(13, cv.size()) assertEquals(13, cv.size())
assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME)) assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME))

View file

@ -20,7 +20,7 @@ class BookmarkLocationControllerTest {
@Before @Before
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(bookmarkDao!!.allBookmarksLocations) whenever(bookmarkDao!!.getAllBookmarksLocations())
.thenReturn(mockBookmarkList) .thenReturn(mockBookmarkList)
} }

View file

@ -463,7 +463,7 @@ class NearbyParentFragmentPresenterTest {
nearbyPlacesInfo.searchLatLng = latestLocation nearbyPlacesInfo.searchLatLng = latestLocation
nearbyPlacesInfo.placeList = emptyList<Place>() nearbyPlacesInfo.placeList = emptyList<Place>()
whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList()) whenever(bookmarkLocationsDao.getAllBookmarksLocations()).thenReturn(Collections.emptyList())
nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null) nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null)
Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false) Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false)
} }