Migrated bookmarks locations to Kotlin and adapt room database (#6148)

* Rename .java to .kt

* 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.

* Refactor: Migrate to Room Database for Bookmark Locations

This commit migrates the bookmark locations functionality from a custom content provider to Room database.

Key changes:

*   **Removal of `BookmarkLocationsContentProvider`:** This class, which previously handled data storage, has been removed.
*   **Introduction of `BookmarksLocations`:** This data class now represents a bookmarked location, serving as the Room entity.
*   **Creation of `BookmarkLocationsDao`:** This Room DAO handles database interactions for bookmark locations, including:
    *   Adding, deleting, and querying bookmarked locations.
    *   Checking if a location is already bookmarked.
    *   Updating the bookmark status of a location.
    *   Retrieving all bookmarked locations as `Place` objects.
*   **`BookmarkLocationsViewModel`:** Added to manage the data layer for bookmark locations
*   **`NearbyUtil`:** Created a Util class for Nearby to manage the bookmark locations.
*   **Updates in `PlaceAdapter` and `PlaceAdapterDelegate`:** These classes have been modified to work with the new Room-based data layer.
*   **Updates in `AppDatabase`:** The database now includes `BookmarksLocations` as an entity and exposes the `bookmarkLocationsDao`.
*   **Updates in `FragmentBuilderModule` and `CommonsApplicationModule`**: for DI
*   **Removal of `DBOpenHelper` upgrade for locations**: as it is no longer needed
* **Updates in `NearbyParentFragmentPresenter`**: refactored the logic to use the dao functions
* **Updates in `NearbyParentFragment`**: refactored the logic to use the util and dao functions
* **Update in `BookmarkLocationsController`**: removed as its no longer needed
* **Add `toPlace` and `toBookmarksLocations`**: extension functions to map between data class and entities
* **Update in `CommonsApplication`**: to remove old db table.

* Refactor: Improve bookmark location handling and update database version

This commit includes the following changes:

-   Updates the database version to 20.
-   Refactors bookmark location handling within `NearbyParentFragment` to improve logic and efficiency.
-   Introduces `getBookmarkLocationExists` in `NearbyUtil` to directly update bookmark icons in the bottom sheet adapter.
-   Removes unused provider.
-   Adjusts `BookmarkLocationsFragment`'s `PlaceAdapter` to use `lifecycleScope` for better coroutine management.
-   Refactors `updateBookmarkLocation` in `NearbyParentFragmentPresenter`.

* Toggle bookmark icon in BottomSheetAdapter instead of finding the location each time in the bookmark

* Update bookmark button image in `BottomSheetAdapter`
* Add new toggle function to `BottomSheetAdapter`
* Call the toggle function in `NearbyParentFragment`

* Refactor: Load bookmarked locations using Flow

*   `BookmarkLocationsController`: Changed to use `Flow` to load bookmarked locations.
*   `BookmarkLocationsDao`: Changed to return `Flow` of bookmark location instead of a list.
*   `BookmarkLocationsFragment`: Used `LifecycleScope` to collect data from flow and display it.
*   Removed unused `getAllBookmarkLocations()` from `BookmarkLocationsViewModel`.
*   Used `map` in `BookmarkLocationsDao` to convert from `BookmarksLocations` to `Place` list.

* Loading locations data in fragment

*   BookmarkLocationsController: Changed `loadFavoritesLocations` to be a suspend function.
*   BookmarkLocationsFragment: Updated the `initList` function to call the controller's suspend function.
*   BookmarkLocationsDao: Changed `getAllBookmarksLocations` and `getAllBookmarksLocationsPlace` to be suspend functions.

* Refactor BookmarkLocationControllerTest and related files to use coroutines

*  Migrated `bookmarkDao!!.getAllBookmarksLocations()` to `bookmarkDao!!.getAllBookmarksLocationsPlace()` and added `runBlocking`
*  Added `runBlocking` for `loadBookmarkedLocations()` and `testInitNonEmpty()`
*  Added `runBlocking` for `getAllBookmarksLocations()` and update `updateMapMarkers`

These changes improve the test structure by ensuring the database functions are executed within a coroutine context.

* Refactor BookmarkLocationsFragment and add tests for BookmarkLocationsDao

*   Moved `initList` to `BookmarkLocationsFragment` for better lifecycle handling.
*   Added test case for `BookmarkLocationsFragment`'s onResume.
*   Added test cases for `BookmarkLocationsDao` operations like adding, retrieving, finding, deleting and updating bookmarks.

* Refactor BookmarkLocationsFragment to load favorites only when view is not null

* BookmarkLocationsFragment: load favorites locations only when view is not null.
* BookmarkLocationsFragmentTest: added spy and verify to test onResume() call initList() method.

* Refactor database and add migration

*   `AppDatabase`: Updated to use room migration.
*   `CommonsApplicationModule`: added migration from version 19 to 20 to `appDatabase`

* Rename .java to .kt

* 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.

* Refactor: Migrate to Room Database for Bookmark Locations

This commit migrates the bookmark locations functionality from a custom content provider to Room database.

Key changes:

*   **Removal of `BookmarkLocationsContentProvider`:** This class, which previously handled data storage, has been removed.
*   **Introduction of `BookmarksLocations`:** This data class now represents a bookmarked location, serving as the Room entity.
*   **Creation of `BookmarkLocationsDao`:** This Room DAO handles database interactions for bookmark locations, including:
    *   Adding, deleting, and querying bookmarked locations.
    *   Checking if a location is already bookmarked.
    *   Updating the bookmark status of a location.
    *   Retrieving all bookmarked locations as `Place` objects.
*   **`BookmarkLocationsViewModel`:** Added to manage the data layer for bookmark locations
*   **`NearbyUtil`:** Created a Util class for Nearby to manage the bookmark locations.
*   **Updates in `PlaceAdapter` and `PlaceAdapterDelegate`:** These classes have been modified to work with the new Room-based data layer.
*   **Updates in `AppDatabase`:** The database now includes `BookmarksLocations` as an entity and exposes the `bookmarkLocationsDao`.
*   **Updates in `FragmentBuilderModule` and `CommonsApplicationModule`**: for DI
*   **Removal of `DBOpenHelper` upgrade for locations**: as it is no longer needed
* **Updates in `NearbyParentFragmentPresenter`**: refactored the logic to use the dao functions
* **Updates in `NearbyParentFragment`**: refactored the logic to use the util and dao functions
* **Update in `BookmarkLocationsController`**: removed as its no longer needed
* **Add `toPlace` and `toBookmarksLocations`**: extension functions to map between data class and entities
* **Update in `CommonsApplication`**: to remove old db table.

* Refactor: Improve bookmark location handling and update database version

This commit includes the following changes:

-   Updates the database version to 20.
-   Refactors bookmark location handling within `NearbyParentFragment` to improve logic and efficiency.
-   Introduces `getBookmarkLocationExists` in `NearbyUtil` to directly update bookmark icons in the bottom sheet adapter.
-   Removes unused provider.
-   Adjusts `BookmarkLocationsFragment`'s `PlaceAdapter` to use `lifecycleScope` for better coroutine management.
-   Refactors `updateBookmarkLocation` in `NearbyParentFragmentPresenter`.

* Toggle bookmark icon in BottomSheetAdapter instead of finding the location each time in the bookmark

* Update bookmark button image in `BottomSheetAdapter`
* Add new toggle function to `BottomSheetAdapter`
* Call the toggle function in `NearbyParentFragment`

* Refactor: Load bookmarked locations using Flow

*   `BookmarkLocationsController`: Changed to use `Flow` to load bookmarked locations.
*   `BookmarkLocationsDao`: Changed to return `Flow` of bookmark location instead of a list.
*   `BookmarkLocationsFragment`: Used `LifecycleScope` to collect data from flow and display it.
*   Removed unused `getAllBookmarkLocations()` from `BookmarkLocationsViewModel`.
*   Used `map` in `BookmarkLocationsDao` to convert from `BookmarksLocations` to `Place` list.

* Loading locations data in fragment

*   BookmarkLocationsController: Changed `loadFavoritesLocations` to be a suspend function.
*   BookmarkLocationsFragment: Updated the `initList` function to call the controller's suspend function.
*   BookmarkLocationsDao: Changed `getAllBookmarksLocations` and `getAllBookmarksLocationsPlace` to be suspend functions.

* Refactor BookmarkLocationControllerTest and related files to use coroutines

*  Migrated `bookmarkDao!!.getAllBookmarksLocations()` to `bookmarkDao!!.getAllBookmarksLocationsPlace()` and added `runBlocking`
*  Added `runBlocking` for `loadBookmarkedLocations()` and `testInitNonEmpty()`
*  Added `runBlocking` for `getAllBookmarksLocations()` and update `updateMapMarkers`

These changes improve the test structure by ensuring the database functions are executed within a coroutine context.

* Refactor BookmarkLocationsFragment and add tests for BookmarkLocationsDao

*   Moved `initList` to `BookmarkLocationsFragment` for better lifecycle handling.
*   Added test case for `BookmarkLocationsFragment`'s onResume.
*   Added test cases for `BookmarkLocationsDao` operations like adding, retrieving, finding, deleting and updating bookmarks.

* Refactor BookmarkLocationsFragment to load favorites only when view is not null

* BookmarkLocationsFragment: load favorites locations only when view is not null.
* BookmarkLocationsFragmentTest: added spy and verify to test onResume() call initList() method.

* Refactor database and add migration

*   `AppDatabase`: Updated to use room migration.
*   `CommonsApplicationModule`: added migration from version 19 to 20 to `appDatabase`

* Resolve conflicts and attach the `commons.db` with `common_room.db` during the migration to persist the data

* Refactor database migration to handle null values and use direct insertion

*   Modify the database migration from version 19 to 20 to properly handle null values and use direct data insertion.
*   Create a new table `bookmarks_locations` with `NOT NULL` constraints.
*   Directly insert data into the new table from old database, safely handling `NULL` values by using `DEFAULT` empty string value.
*   Close old database cursor and the database connection.
* Drop the old `bookmarksLocations` table.

* Refactor database schema to delete `bookmarksLocations` table and migrate data

*   Delete `bookmarksLocations` table from the database during schema upgrade
*   Migrate data from `bookmarksLocations` to `bookmarks_locations` in new schema
*   Add `INSERT OR REPLACE` to prevent duplication during migration
*   Delete `CONTRIBUTIONS_TABLE` and `BOOKMARKS_LOCATIONS` in the application start-up to have a fresh DB for first start-up after update

* Update sqlite-based database version to 22.

* Update sqlite-based database version to 22.

* Refactor CommonsApplicationModule to utilize application context

* Initialize and use application context directly

* Utilize lateinit for application context

* Added line breaks for improved readability.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Saifuddin Adenwala 2025-02-24 19:28:26 +05:30 committed by GitHub
parent 1c4797d3aa
commit 1c7dce9e12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 702 additions and 946 deletions

View file

@ -232,12 +232,6 @@
android:exported="false"
android:label="@string/provider_bookmarks"
android:syncable="false" />
<provider
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<provider
android:name=".bookmarks.items.BookmarkItemsContentProvider"
android:authorities="${applicationId}.bookmarks.items.contentprovider"

View file

@ -247,13 +247,17 @@ class CommonsApplication : MultiDexApplication() {
DBOpenHelper.CONTRIBUTIONS_TABLE
) //Delete the contributions table in the existing db on older versions
dbOpenHelper.deleteTable(
db,
DBOpenHelper.BOOKMARKS_LOCATIONS
)
try {
contributionDao.deleteAll()
} catch (e: SQLiteException) {
Timber.e(e)
}
BookmarkPicturesDao.Table.onDelete(db)
BookmarkLocationsDao.Table.onDelete(db)
BookmarkItemsDao.Table.onDelete(db)
}

View file

@ -1,119 +0,0 @@
package fr.free.nrw.commons.bookmarks.locations;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though)
import android.net.Uri;
import android.text.TextUtils;
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
*/
public class BookmarkLocationsContentProvider extends CommonsDaggerContentProvider {
private static final String BASE_PATH = "bookmarksLocations";
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY + "/" + BASE_PATH);
/**
* Append bookmark locations name to the base uri
*/
public static Uri uriForName(String name) {
return Uri.parse(BASE_URI.toString() + "/" + 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;
}
/**
* Handles the insertion of new bookmark locations record to local SQLite Database
*/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = sqlDB.insert(BookmarkLocationsDao.Table.TABLE_NAME, null, contentValues);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
@SuppressWarnings("ConstantConditions")
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows;
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
rows = db.delete(TABLE_NAME,
"location_name = ?",
new String[]{uri.getLastPathSegment()}
);
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
}

View file

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

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.bookmarks.locations
import fr.free.nrw.commons.nearby.Place
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BookmarkLocationsController @Inject constructor(
private val bookmarkLocationDao: BookmarkLocationsDao
) {
/**
* Load bookmarked locations from the database.
* @return a list of Place objects.
*/
suspend fun loadFavoritesLocations(): List<Place> =
bookmarkLocationDao.getAllBookmarksLocationsPlace()
}

View file

@ -1,313 +0,0 @@
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 fr.free.nrw.commons.nearby.NearbyController;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
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
*
* @return list of Place
*/
@NonNull
public List<Place> getAllBookmarksLocations() {
List<Place> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkLocationsContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
null);
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
/**
* Look for a place in bookmarks table in order to insert or delete it
*
* @param bookmarkLocation : Place object
* @return is Place now fav ?
*/
public boolean updateBookmarkLocation(Place bookmarkLocation) {
boolean bookmarkExists = findBookmarkLocation(bookmarkLocation);
if (bookmarkExists) {
deleteBookmarkLocation(bookmarkLocation);
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false);
} else {
addBookmarkLocation(bookmarkLocation);
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true);
}
return !bookmarkExists;
}
/**
* Add a Place to bookmarks table
*
* @param bookmarkLocation : Place to add
*/
private void addBookmarkLocation(Place bookmarkLocation) {
ContentProviderClient db = clientProvider.get();
try {
db.insert(BASE_URI, toContentValues(bookmarkLocation));
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Delete a Place from bookmarks table
*
* @param bookmarkLocation : Place to delete
*/
private void deleteBookmarkLocation(Place bookmarkLocation) {
ContentProviderClient db = clientProvider.get();
try {
db.delete(BookmarkLocationsContentProvider.uriForName(bookmarkLocation.name), null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find a Place from database based on its name
*
* @param bookmarkLocation : Place to find
* @return boolean : is Place in database ?
*/
public boolean findBookmarkLocation(Place bookmarkLocation) {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkLocationsContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?",
new String[]{bookmarkLocation.name},
null);
if (cursor != null && cursor.moveToFirst()) {
return true;
}
} 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 false;
}
@SuppressLint("Range")
@NonNull
Place fromCursor(final Cursor cursor) {
final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)),
cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F);
final Sitelinks.Builder builder = new Sitelinks.Builder();
builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK)));
builder.setWikidataLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIDATA_LINK)));
builder.setCommonsLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_COMMONS_LINK)));
return new Place(
cursor.getString(cursor.getColumnIndex(Table.COLUMN_LANGUAGE)),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
Label.fromText((cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT)))),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
location,
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)),
builder.build(),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)),
Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(Table.COLUMN_EXISTS)))
);
}
private ContentValues toContentValues(Place bookmarkLocation) {
ContentValues cv = new ContentValues();
cv.put(BookmarkLocationsDao.Table.COLUMN_NAME, bookmarkLocation.getName());
cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage());
cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription());
cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory());
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getText() : "");
cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getIcon() : null);
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.getWikipediaLink().toString());
cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.getWikidataLink().toString());
cv.put(BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.getCommonsLink().toString());
cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude());
cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude());
cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic);
cv.put(BookmarkLocationsDao.Table.COLUMN_EXISTS, bookmarkLocation.exists.toString());
return cv;
}
public static class Table {
public static final String TABLE_NAME = "bookmarksLocations";
static final String COLUMN_NAME = "location_name";
static final String COLUMN_LANGUAGE = "location_language";
static final String COLUMN_DESCRIPTION = "location_description";
static final String COLUMN_LAT = "location_lat";
static final String COLUMN_LONG = "location_long";
static final String COLUMN_CATEGORY = "location_category";
static final String COLUMN_LABEL_TEXT = "location_label_text";
static final String COLUMN_LABEL_ICON = "location_label_icon";
static final String COLUMN_IMAGE_URL = "location_image_url";
static final String COLUMN_WIKIPEDIA_LINK = "location_wikipedia_link";
static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link";
static final String COLUMN_COMMONS_LINK = "location_commons_link";
static final String COLUMN_PIC = "location_pic";
static final String COLUMN_EXISTS = "location_exists";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_NAME,
COLUMN_LANGUAGE,
COLUMN_DESCRIPTION,
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;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_NAME + " STRING PRIMARY KEY,"
+ COLUMN_LANGUAGE + " STRING,"
+ COLUMN_DESCRIPTION + " STRING,"
+ COLUMN_CATEGORY + " STRING,"
+ COLUMN_LABEL_TEXT + " STRING,"
+ COLUMN_LABEL_ICON + " INTEGER,"
+ COLUMN_LAT + " DOUBLE,"
+ COLUMN_LONG + " DOUBLE,"
+ COLUMN_IMAGE_URL + " STRING,"
+ COLUMN_WIKIPEDIA_LINK + " STRING,"
+ COLUMN_WIKIDATA_LINK + " STRING,"
+ COLUMN_COMMONS_LINK + " STRING,"
+ COLUMN_PIC + " STRING,"
+ COLUMN_EXISTS + " STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onUpdate(final SQLiteDatabase db, int from, final int to) {
Timber.d("bookmarksLocations db is updated from:"+from+", to:"+to);
if (from == to) {
return;
}
if (from < 7) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 7) {
// table added in version 8
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if (from < 10) {
from++;
onUpdate(db, from, to);
return;
}
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 {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;");
}catch (SQLiteException exception){
Timber.e(exception);//
}
return;
}
if (from >= 12) {
try {
db.execSQL(
"ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;");
} catch (SQLiteException exception) {
Timber.e(exception);
}
}
if (from >= 13){
try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;");
} catch (SQLiteException exception){
Timber.e(exception);
}
}
if (from >= 14){
try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;");
} catch (SQLiteException exception){
Timber.e(exception);
}
}
}
}
}

View file

@ -0,0 +1,65 @@
package fr.free.nrw.commons.bookmarks.locations
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import fr.free.nrw.commons.nearby.NearbyController
import fr.free.nrw.commons.nearby.Place
/**
* DAO for managing bookmark locations in the database.
*/
@Dao
abstract class BookmarkLocationsDao {
/**
* Adds or updates a bookmark location in the database.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun addBookmarkLocation(bookmarkLocation: BookmarksLocations)
/**
* Fetches all bookmark locations from the database.
*/
@Query("SELECT * FROM bookmarks_locations")
abstract suspend fun getAllBookmarksLocations(): List<BookmarksLocations>
/**
* Checks if a bookmark location exists by name.
*/
@Query("SELECT EXISTS (SELECT 1 FROM bookmarks_locations WHERE location_name = :name)")
abstract suspend fun findBookmarkLocation(name: String): Boolean
/**
* Deletes a bookmark location from the database.
*/
@Delete
abstract suspend fun deleteBookmarkLocation(bookmarkLocation: BookmarksLocations)
/**
* Adds or removes a bookmark location and updates markers.
* @return `true` if added, `false` if removed.
*/
suspend fun updateBookmarkLocation(bookmarkLocation: Place): Boolean {
val exists = findBookmarkLocation(bookmarkLocation.name)
if (exists) {
deleteBookmarkLocation(bookmarkLocation.toBookmarksLocations())
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false)
} else {
addBookmarkLocation(bookmarkLocation.toBookmarksLocations())
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true)
}
return !exists
}
/**
* Fetches all bookmark locations as `Place` objects.
*/
suspend fun getAllBookmarksLocationsPlace(): List<Place> {
return getAllBookmarksLocations().map { it.toPlace() }
}
}

View file

@ -1,137 +0,0 @@
package fr.free.nrw.commons.bookmarks.locations;
import android.Manifest.permission;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding;
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;
@Inject BookmarkLocationsController controller;
@Inject ContributionController contributionController;
@Inject BookmarkLocationsDao bookmarkLocationDao;
@Inject CommonPlaceClickActions commonPlaceClickActions;
private PlaceAdapter adapter;
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult =
registerForActivityResult(new StartActivityForResult(),
result -> {
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
});
});
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult =
registerForActivityResult(new StartActivityForResult(),
result -> {
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
});
});
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
@Override
public void onActivityResult(Map<String, Boolean> result) {
boolean areAllGranted = true;
for(final boolean b : result.values()) {
areAllGranted = areAllGranted && b;
}
if (areAllGranted) {
contributionController.locationPermissionCallback.onLocationPermissionGranted();
} else {
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
} else {
contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied));
}
}
}
});
/**
* Create an instance of the fragment with the right bundle parameters
* @return an instance of the fragment
*/
public static BookmarkLocationsFragment newInstance() {
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,
inAppCameraLocationPermissionLauncher,
galleryPickLauncherForResult,
cameraPickLauncherForResult
);
binding.listView.setAdapter(adapter);
}
@Override
public void onResume() {
super.onResume();
initList();
}
/**
* Initialize the recycler view with bookmarked locations
*/
private void initList() {
List<Place> places = controller.loadFavoritesLocations();
adapter.setItems(places);
binding.loadingImagesProgressBar.setVisibility(View.GONE);
if (places.size() <= 0) {
binding.statusMessage.setText(R.string.bookmark_empty);
binding.statusMessage.setVisibility(View.VISIBLE);
} else {
binding.statusMessage.setVisibility(View.GONE);
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,161 @@
package fr.free.nrw.commons.bookmarks.locations
import android.Manifest.permission
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.android.support.DaggerFragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding
import fr.free.nrw.commons.filepicker.FilePicker
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter
import kotlinx.coroutines.launch
import javax.inject.Inject
class BookmarkLocationsFragment : DaggerFragment() {
private var binding: FragmentBookmarksLocationsBinding? = null
@Inject lateinit var controller: BookmarkLocationsController
@Inject lateinit var contributionController: ContributionController
@Inject lateinit var bookmarkLocationDao: BookmarkLocationsDao
@Inject lateinit var commonPlaceClickActions: CommonPlaceClickActions
private lateinit var inAppCameraLocationPermissionLauncher:
ActivityResultLauncher<Array<String>>
private lateinit var adapter: PlaceAdapter
private val cameraPickLauncherForResult =
registerForActivityResult(StartActivityForResult()) { result ->
contributionController.handleActivityResultWithCallback(
requireActivity(),
object: FilePicker.HandleActivityResult {
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
contributionController.onPictureReturnedFromCamera(
result,
requireActivity(),
callbacks
)
}
}
)
}
private val galleryPickLauncherForResult =
registerForActivityResult(StartActivityForResult()) { result ->
contributionController.handleActivityResultWithCallback(
requireActivity(),
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 {
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
contributionController.handleShowRationaleFlowCameraLocation(
requireActivity(),
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
} else {
contributionController.locationPermissionCallback
?.onLocationPermissionDenied(
getString(R.string.in_app_camera_location_permission_denied)
)
}
}
}
adapter = PlaceAdapter(
bookmarkLocationDao,
lifecycleScope,
{ },
{ place, _ ->
adapter.remove(place)
},
commonPlaceClickActions,
inAppCameraLocationPermissionLauncher,
galleryPickLauncherForResult,
cameraPickLauncherForResult
)
binding?.listView?.adapter = adapter
}
override fun onResume() {
super.onResume()
initList()
}
fun initList() {
var places: List<Place>
if(view != null) {
viewLifecycleOwner.lifecycleScope.launch {
places = controller.loadFavoritesLocations()
updateUIList(places)
}
}
}
private fun updateUIList(places: List<Place>) {
adapter.items = places
binding?.loadingImagesProgressBar?.visibility = View.GONE
if (places.isEmpty()) {
binding?.statusMessage?.text = getString(R.string.bookmark_empty)
binding?.statusMessage?.visibility = View.VISIBLE
} else {
binding?.statusMessage?.visibility = View.GONE
}
}
override fun onDestroy() {
super.onDestroy()
// Make sure to null out the binding to avoid memory leaks
binding = null
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.bookmarks.locations
import androidx.lifecycle.ViewModel
import fr.free.nrw.commons.nearby.Place
import kotlinx.coroutines.flow.Flow
class BookmarkLocationsViewModel(
private val bookmarkLocationsDao: BookmarkLocationsDao
): ViewModel() {
// fun getAllBookmarkLocations(): List<Place> {
// return bookmarkLocationsDao.getAllBookmarksLocationsPlace()
// }
}

View file

@ -0,0 +1,72 @@
package fr.free.nrw.commons.bookmarks.locations
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
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
@Entity(tableName = "bookmarks_locations")
data class BookmarksLocations(
@PrimaryKey @ColumnInfo(name = "location_name") val locationName: String,
@ColumnInfo(name = "location_language") val locationLanguage: String,
@ColumnInfo(name = "location_description") val locationDescription: String,
@ColumnInfo(name = "location_lat") val locationLat: Double,
@ColumnInfo(name = "location_long") val locationLong: Double,
@ColumnInfo(name = "location_category") val locationCategory: String,
@ColumnInfo(name = "location_label_text") val locationLabelText: String,
@ColumnInfo(name = "location_label_icon") val locationLabelIcon: Int?,
@ColumnInfo(name = "location_image_url") val locationImageUrl: String,
@ColumnInfo(name = "location_wikipedia_link") val locationWikipediaLink: String,
@ColumnInfo(name = "location_wikidata_link") val locationWikidataLink: String,
@ColumnInfo(name = "location_commons_link") val locationCommonsLink: String,
@ColumnInfo(name = "location_pic") val locationPic: String,
@ColumnInfo(name = "location_exists") val locationExists: Boolean
)
fun BookmarksLocations.toPlace(): Place {
val location = LatLng(
locationLat,
locationLong,
1F
)
val builder = Sitelinks.Builder().apply {
setWikipediaLink(locationWikipediaLink)
setWikidataLink(locationWikidataLink)
setCommonsLink(locationCommonsLink)
}
return Place(
locationLanguage,
locationName,
Label.fromText(locationLabelText),
locationDescription,
location,
locationCategory,
builder.build(),
locationPic,
locationExists
)
}
fun Place.toBookmarksLocations(): BookmarksLocations {
return BookmarksLocations(
locationName = name,
locationLanguage = language,
locationDescription = longDescription,
locationCategory = category,
locationLat = location.latitude,
locationLong = location.longitude,
locationLabelText = label?.text ?: "",
locationLabelIcon = label?.icon,
locationImageUrl = pic,
locationWikipediaLink = siteLinks.wikipediaLink.toString(),
locationWikidataLink = siteLinks.wikidataLink.toString(),
locationCommonsLink = siteLinks.commonsLink.toString(),
locationPic = pic,
locationExists = exists
)
}

View file

@ -18,8 +18,9 @@ class DBOpenHelper(
companion object {
private const val DATABASE_NAME = "commons.db"
private const val DATABASE_VERSION = 21
private const val DATABASE_VERSION = 22
const val CONTRIBUTIONS_TABLE = "contributions"
const val BOOKMARKS_LOCATIONS = "bookmarksLocations"
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
}
@ -30,7 +31,6 @@ class DBOpenHelper(
override fun onCreate(db: SQLiteDatabase) {
CategoryDao.Table.onCreate(db)
BookmarkPicturesDao.Table.onCreate(db)
BookmarkLocationsDao.Table.onCreate(db)
BookmarkItemsDao.Table.onCreate(db)
RecentSearchesDao.Table.onCreate(db)
RecentLanguagesDao.Table.onCreate(db)
@ -39,11 +39,11 @@ class DBOpenHelper(
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
CategoryDao.Table.onUpdate(db, from, to)
BookmarkPicturesDao.Table.onUpdate(db, from, to)
BookmarkLocationsDao.Table.onUpdate(db, from, to)
BookmarkItemsDao.Table.onUpdate(db, from, to)
RecentSearchesDao.Table.onUpdate(db, from, to)
RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE)
deleteTable(db, BOOKMARKS_LOCATIONS)
}
/**

View file

@ -1,10 +1,16 @@
package fr.free.nrw.commons.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarksLocations
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
@ -23,8 +29,8 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
*
*/
@Database(
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class],
version = 19,
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class],
version = 20,
exportSchema = false,
)
@TypeConverters(Converters::class)
@ -42,4 +48,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun ReviewDao(): ReviewDao
abstract fun bookmarkCategoriesDao(): BookmarkCategoriesDao
abstract fun bookmarkLocationsDao(): BookmarkLocationsDao
}

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.view.inputmethod.InputMethodManager
import androidx.collection.LruCache
import androidx.room.Room.databaseBuilder
@ -16,6 +17,7 @@ import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
@ -36,6 +38,7 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.util.Objects
import javax.inject.Named
import javax.inject.Singleton
@ -49,6 +52,11 @@ import javax.inject.Singleton
@Module
@Suppress("unused")
open class CommonsApplicationModule(private val applicationContext: Context) {
init {
appContext = applicationContext
}
@Provides
fun providesImageFileLoader(context: Context): ImageFileLoader =
ImageFileLoader(context)
@ -110,11 +118,6 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY)
@Provides
@Named("bookmarksLocation")
fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? =
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY)
@Provides
@Named("bookmarksItem")
fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? =
@ -196,7 +199,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
applicationContext,
AppDatabase::class.java,
"commons_room.db"
).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
).addMigrations(
MIGRATION_1_2,
MIGRATION_19_TO_20
).fallbackToDestructiveMigration().build()
@Provides
fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao =
@ -206,6 +212,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao =
appDatabase.PlaceDao()
@Provides
fun providesBookmarkLocationsDao(appDatabase: AppDatabase): BookmarkLocationsDao =
appDatabase.bookmarkLocationsDao()
@Provides
fun providesDepictDao(appDatabase: AppDatabase): DepictsDao =
appDatabase.DepictsDao()
@ -239,6 +249,9 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
const val IO_THREAD: String = "io_thread"
const val MAIN_THREAD: String = "main_thread"
lateinit var appContext: Context
private set
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
@ -246,5 +259,101 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
)
}
}
private val MIGRATION_19_TO_20 = object : Migration(19, 20) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS bookmarks_locations (
location_name TEXT NOT NULL PRIMARY KEY,
location_language TEXT NOT NULL,
location_description TEXT NOT NULL,
location_lat REAL NOT NULL,
location_long REAL NOT NULL,
location_category TEXT NOT NULL,
location_label_text TEXT NOT NULL,
location_label_icon INTEGER,
location_image_url TEXT NOT NULL DEFAULT '',
location_wikipedia_link TEXT NOT NULL,
location_wikidata_link TEXT NOT NULL,
location_commons_link TEXT NOT NULL,
location_pic TEXT NOT NULL,
location_exists INTEGER NOT NULL CHECK(location_exists IN (0, 1))
)
"""
)
val oldDbPath = appContext.getDatabasePath("commons.db").path
val oldDb = SQLiteDatabase
.openDatabase(oldDbPath, null, SQLiteDatabase.OPEN_READONLY)
val cursor = oldDb.rawQuery("SELECT * FROM bookmarksLocations", null)
while (cursor.moveToNext()) {
val locationName =
cursor.getString(cursor.getColumnIndexOrThrow("location_name"))
val locationLanguage =
cursor.getString(cursor.getColumnIndexOrThrow("location_language"))
val locationDescription =
cursor.getString(cursor.getColumnIndexOrThrow("location_description"))
val locationCategory =
cursor.getString(cursor.getColumnIndexOrThrow("location_category"))
val locationLabelText =
cursor.getString(cursor.getColumnIndexOrThrow("location_label_text"))
val locationLabelIcon =
cursor.getInt(cursor.getColumnIndexOrThrow("location_label_icon"))
val locationLat =
cursor.getDouble(cursor.getColumnIndexOrThrow("location_lat"))
val locationLong =
cursor.getDouble(cursor.getColumnIndexOrThrow("location_long"))
// Handle NULL values safely
val locationImageUrl =
cursor.getString(
cursor.getColumnIndexOrThrow("location_image_url")
) ?: ""
val locationWikipediaLink =
cursor.getString(
cursor.getColumnIndexOrThrow("location_wikipedia_link")
) ?: ""
val locationWikidataLink =
cursor.getString(
cursor.getColumnIndexOrThrow("location_wikidata_link")
) ?: ""
val locationCommonsLink =
cursor.getString(
cursor.getColumnIndexOrThrow("location_commons_link")
) ?: ""
val locationPic =
cursor.getString(
cursor.getColumnIndexOrThrow("location_pic")
) ?: ""
val locationExists =
cursor.getInt(
cursor.getColumnIndexOrThrow("location_exists")
)
db.execSQL(
"""
INSERT OR REPLACE INTO bookmarks_locations (
location_name, location_language, location_description, location_category,
location_label_text, location_label_icon, location_lat, location_long,
location_image_url, location_wikipedia_link, location_wikidata_link,
location_commons_link, location_pic, location_exists
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
arrayOf(
locationName, locationLanguage, locationDescription, locationCategory,
locationLabelText, locationLabelIcon, locationLat, locationLong,
locationImageUrl, locationWikipediaLink, locationWikidataLink,
locationCommonsLink, locationPic, locationExists
)
)
}
cursor.close()
oldDb.close()
}
}
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
import fr.free.nrw.commons.category.CategoryContentProvider
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider
@ -26,9 +25,6 @@ abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider
@ContributesAndroidInjector
abstract fun bindBookmarkLocationContentProvider(): BookmarkLocationsContentProvider
@ContributesAndroidInjector
abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider

View file

@ -68,7 +68,21 @@ class BottomSheetAdapter(
item.imageResourceId == R.drawable.ic_round_star_border_24px
) {
item.imageResourceId = icon
this.notifyItemChanged(index)
notifyItemChanged(index)
return
}
}
}
fun toggleBookmarkIcon() {
itemList.forEachIndexed { index, item ->
if(item.imageResourceId == R.drawable.ic_round_star_filled_24px) {
item.imageResourceId = R.drawable.ic_round_star_border_24px
notifyItemChanged(index)
return
} else if(item.imageResourceId == R.drawable.ic_round_star_border_24px){
item.imageResourceId = R.drawable.ic_round_star_filled_24px
notifyItemChanged(index)
return
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.nearby
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import kotlinx.coroutines.launch
import timber.log.Timber
object NearbyUtil {
fun getBookmarkLocationExists(
bookmarksLocationsDao: BookmarkLocationsDao,
name: String,
scope: LifecycleCoroutineScope?,
bottomSheetAdapter: BottomSheetAdapter,
) {
scope?.launch {
val isBookmarked = bookmarksLocationsDao.findBookmarkLocation(name)
Timber.i("isBookmarked: $isBookmarked")
if (isBookmarked) {
bottomSheetAdapter.updateBookmarkIcon(R.drawable.ic_round_star_filled_24px)
} else {
bottomSheetAdapter.updateBookmarkIcon(R.drawable.ic_round_star_border_24px)
}
}
}
}

View file

@ -7,6 +7,7 @@ import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.RelativeLayout
import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
@ -16,9 +17,11 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.databinding.ItemPlaceBinding
import kotlinx.coroutines.launch
fun placeAdapterDelegate(
bookmarkLocationDao: BookmarkLocationsDao,
scope: LifecycleCoroutineScope?,
onItemClick: ((Place) -> Unit)? = null,
onCameraClicked: (Place, ActivityResultLauncher<Array<String>>, ActivityResultLauncher<Intent>) -> Unit,
onCameraLongPressed: () -> Boolean,
@ -61,7 +64,10 @@ fun placeAdapterDelegate(
nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item, galleryPickLauncherForResult) }
nearbyButtonLayout.galleryButton.setOnLongClickListener { onGalleryLongPressed() }
bookmarkButtonImage.setOnClickListener {
val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item)
var isBookmarked = false
scope?.launch {
isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item)
}
bookmarkButtonImage.setImageResource(
if (isBookmarked) R.drawable.ic_round_star_filled_24px else R.drawable.ic_round_star_border_24px,
)
@ -93,14 +99,16 @@ fun placeAdapterDelegate(
GONE
}
scope?.launch {
bookmarkButtonImage.setImageResource(
if (bookmarkLocationDao.findBookmarkLocation(item)) {
if (bookmarkLocationDao.findBookmarkLocation(item.name)) {
R.drawable.ic_round_star_filled_24px
} else {
R.drawable.ic_round_star_border_24px
},
)
}
}
nearbyButtonLayout.directionsButton.setOnLongClickListener { onDirectionsLongPressed() }
}
}

View file

@ -134,7 +134,7 @@ public interface NearbyParentFragmentContract {
void setAdvancedQuery(String query);
void toggleBookmarkedStatus(Place place);
void toggleBookmarkedStatus(Place place, LifecycleCoroutineScope scope);
void handleMapScrolled(LifecycleCoroutineScope scope, boolean isNetworkAvailable);
}

View file

@ -85,6 +85,7 @@ import fr.free.nrw.commons.nearby.MarkerPlaceGroup
import fr.free.nrw.commons.nearby.NearbyController
import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter
import fr.free.nrw.commons.nearby.NearbyFilterState
import fr.free.nrw.commons.nearby.NearbyUtil
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.PlacesRepository
import fr.free.nrw.commons.nearby.WikidataFeedback
@ -664,21 +665,23 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
private fun initRvNearbyList() {
binding!!.bottomSheetNearby.rvNearbyList.layoutManager = LinearLayoutManager(context)
adapter = PlaceAdapter(
bookmarkLocationDao!!,
{ place: Place ->
bookmarkLocationsDao = bookmarkLocationDao,
scope = scope,
onPlaceClicked = { place: Place ->
moveCameraToPosition(
GeoPoint(place.location.latitude, place.location.longitude)
GeoPoint(
place.location.latitude,
place.location.longitude
)
)
Unit
},
{ place: Place?, isBookmarked: Boolean? ->
presenter!!.toggleBookmarkedStatus(place)
Unit
onBookmarkClicked = { place: Place?, _: Boolean? ->
presenter!!.toggleBookmarkedStatus(place, scope)
},
commonPlaceClickActions!!,
inAppCameraLocationPermissionLauncher,
galleryPickLauncherForResult,
cameraPickLauncherForResult
commonPlaceClickActions = commonPlaceClickActions,
inAppCameraLocationPermissionLauncher = inAppCameraLocationPermissionLauncher,
galleryPickLauncherForResult = galleryPickLauncherForResult,
cameraPickLauncherForResult = cameraPickLauncherForResult
)
binding!!.bottomSheetNearby.rvNearbyList.adapter = adapter
}
@ -2303,34 +2306,34 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
// TODO: Decide button text for fitting in the screen
(dataList as ArrayList<BottomSheetItem>).add(
BottomSheetItem(
fr.free.nrw.commons.R.drawable.ic_round_star_border_24px,
R.drawable.ic_round_star_border_24px,
""
)
)
(dataList as ArrayList<BottomSheetItem>).add(
BottomSheetItem(
fr.free.nrw.commons.R.drawable.ic_directions_black_24dp,
R.drawable.ic_directions_black_24dp,
resources.getString(fr.free.nrw.commons.R.string.nearby_directions)
)
)
if (place.hasWikidataLink()) {
(dataList as ArrayList<BottomSheetItem>).add(
BottomSheetItem(
fr.free.nrw.commons.R.drawable.ic_wikidata_logo_24dp,
R.drawable.ic_wikidata_logo_24dp,
resources.getString(fr.free.nrw.commons.R.string.nearby_wikidata)
)
)
}
(dataList as ArrayList<BottomSheetItem>).add(
BottomSheetItem(
fr.free.nrw.commons.R.drawable.ic_feedback_black_24dp,
R.drawable.ic_feedback_black_24dp,
resources.getString(fr.free.nrw.commons.R.string.nearby_wikitalk)
)
)
if (place.hasWikipediaLink()) {
(dataList as ArrayList<BottomSheetItem>).add(
BottomSheetItem(
fr.free.nrw.commons.R.drawable.ic_wikipedia_logo_24dp,
R.drawable.ic_wikipedia_logo_24dp,
resources.getString(fr.free.nrw.commons.R.string.nearby_wikipedia)
)
)
@ -2338,7 +2341,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
if (selectedPlace!!.hasCommonsLink()) {
(dataList as ArrayList<BottomSheetItem>).add(
BottomSheetItem(
fr.free.nrw.commons.R.drawable.ic_commons_icon_vector,
R.drawable.ic_commons_icon_vector,
resources.getString(fr.free.nrw.commons.R.string.nearby_commons)
)
)
@ -2566,12 +2569,16 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
}
private fun updateBookmarkButtonImage(place: Place) {
val bookmarkIcon = if (bookmarkLocationDao!!.findBookmarkLocation(place)) {
fr.free.nrw.commons.R.drawable.ic_round_star_filled_24px
} else {
fr.free.nrw.commons.R.drawable.ic_round_star_border_24px
NearbyUtil.getBookmarkLocationExists(
bookmarkLocationDao,
place.getName(),
scope,
bottomSheetAdapter!!
)
}
bottomSheetAdapter!!.updateBookmarkIcon(bookmarkIcon)
private fun toggleBookmarkButtonImage() {
bottomSheetAdapter?.toggleBookmarkIcon()
}
override fun onAttach(context: Context) {
@ -2749,26 +2756,31 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
override fun onBottomSheetItemClick(view: View?, position: Int) {
val item = dataList?.get(position) ?: return // Null check for dataList
when (item.imageResourceId) {
fr.free.nrw.commons.R.drawable.ic_round_star_border_24px,
fr.free.nrw.commons.R.drawable.ic_round_star_filled_24px -> {
presenter?.toggleBookmarkedStatus(selectedPlace)
R.drawable.ic_round_star_border_24px -> {
presenter?.toggleBookmarkedStatus(selectedPlace, scope)
toggleBookmarkButtonImage()
}
R.drawable.ic_round_star_filled_24px -> {
presenter?.toggleBookmarkedStatus(selectedPlace, scope)
toggleBookmarkButtonImage()
selectedPlace?.let { updateBookmarkButtonImage(it) }
}
fr.free.nrw.commons.R.drawable.ic_directions_black_24dp -> {
R.drawable.ic_directions_black_24dp -> {
selectedPlace?.let {
Utils.handleGeoCoordinates(this.context, it.getLocation())
binding?.map?.zoomLevelDouble ?: 0.0
}
}
fr.free.nrw.commons.R.drawable.ic_wikidata_logo_24dp -> {
R.drawable.ic_wikidata_logo_24dp -> {
selectedPlace?.siteLinks?.wikidataLink?.let {
Utils.handleWebUrl(this.context, it)
}
}
fr.free.nrw.commons.R.drawable.ic_feedback_black_24dp -> {
R.drawable.ic_feedback_black_24dp -> {
selectedPlace?.let {
val intent = Intent(this.context, WikidataFeedback::class.java).apply {
putExtra("lat", it.location.latitude)
@ -2780,13 +2792,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
}
}
fr.free.nrw.commons.R.drawable.ic_wikipedia_logo_24dp -> {
R.drawable.ic_wikipedia_logo_24dp -> {
selectedPlace?.siteLinks?.wikipediaLink?.let {
Utils.handleWebUrl(this.context, it)
}
}
fr.free.nrw.commons.R.drawable.ic_commons_icon_vector -> {
R.drawable.ic_commons_icon_vector -> {
selectedPlace?.siteLinks?.commonsLink?.let {
Utils.handleWebUrl(this.context, it)
}
@ -2800,13 +2812,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
override fun onBottomSheetItemLongClick(view: View?, position: Int) {
val item = dataList!![position]
val message = when (item.imageResourceId) {
fr.free.nrw.commons.R.drawable.ic_round_star_border_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark)
fr.free.nrw.commons.R.drawable.ic_round_star_filled_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark)
fr.free.nrw.commons.R.drawable.ic_directions_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_directions)
fr.free.nrw.commons.R.drawable.ic_wikidata_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikidata)
fr.free.nrw.commons.R.drawable.ic_feedback_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikitalk)
fr.free.nrw.commons.R.drawable.ic_wikipedia_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikipedia)
fr.free.nrw.commons.R.drawable.ic_commons_icon_vector -> getString(fr.free.nrw.commons.R.string.nearby_commons)
R.drawable.ic_round_star_border_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark)
R.drawable.ic_round_star_filled_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark)
R.drawable.ic_directions_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_directions)
R.drawable.ic_wikidata_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikidata)
R.drawable.ic_feedback_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikitalk)
R.drawable.ic_wikipedia_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikipedia)
R.drawable.ic_commons_icon_vector -> getString(fr.free.nrw.commons.R.string.nearby_commons)
else -> "Long click"
}
Toast.makeText(this.context, message, Toast.LENGTH_SHORT).show()

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby.fragments
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LifecycleCoroutineScope
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.placeAdapterDelegate
@ -9,6 +10,7 @@ import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
class PlaceAdapter(
bookmarkLocationsDao: BookmarkLocationsDao,
scope: LifecycleCoroutineScope? = null,
onPlaceClicked: ((Place) -> Unit)? = null,
onBookmarkClicked: (Place, Boolean) -> Unit,
commonPlaceClickActions: CommonPlaceClickActions,
@ -18,6 +20,7 @@ class PlaceAdapter(
) : BaseDelegateAdapter<Place>(
placeAdapterDelegate(
bookmarkLocationsDao,
scope,
onPlaceClicked,
commonPlaceClickActions.onCameraClicked(),
commonPlaceClickActions.onCameraLongPressed(),

View file

@ -27,6 +27,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.internal.wait
import timber.log.Timber
@ -136,9 +137,14 @@ class NearbyParentFragmentPresenter
* @param place The place whose bookmarked status is to be toggled. If the place is `null`,
* the operation is skipped.
*/
override fun toggleBookmarkedStatus(place: Place?) {
override fun toggleBookmarkedStatus(
place: Place?,
scope: LifecycleCoroutineScope?
) {
if (place == null) return
val nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place)
var nowBookmarked: Boolean
scope?.launch {
nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place)
bookmarkChangedPlaces.add(place)
val placeIndex =
NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location }
@ -148,13 +154,14 @@ class NearbyParentFragmentPresenter
)
nearbyParentFragmentView.setFilterState()
}
}
override fun attachView(view: NearbyParentFragmentContract.View) {
this.nearbyParentFragmentView = view
nearbyParentFragmentView = view
}
override fun detachView() {
this.nearbyParentFragmentView = DUMMY
nearbyParentFragmentView = DUMMY
}
override fun removeNearbyPreferences(applicationKvStore: JsonKvStore) {
@ -337,7 +344,7 @@ class NearbyParentFragmentPresenter
for (i in 0..updatedGroups.lastIndex) {
val repoPlace = placesRepository.fetchPlace(updatedGroups[i].place.entityID)
if (repoPlace != null && repoPlace.name != null && repoPlace.name != ""){
updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace)
updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace.name)
updatedGroups[i].place.apply {
name = repoPlace.name
isMonument = repoPlace.isMonument
@ -375,7 +382,7 @@ class NearbyParentFragmentPresenter
collectResults.send(
fetchedPlaces.mapIndexed { index, place ->
Pair(indices[index], MarkerPlaceGroup(
bookmarkLocationDao.findBookmarkLocation(place),
bookmarkLocationDao.findBookmarkLocation(place.name),
place
))
}
@ -393,7 +400,10 @@ class NearbyParentFragmentPresenter
onePlaceBatch.add(Pair(i, MarkerPlaceGroup(
bookmarkLocationDao.findBookmarkLocation(
fetchedPlace[0]),fetchedPlace[0])))
fetchedPlace[0].name
),
fetchedPlace[0]
)))
} catch (e: Exception) {
Timber.tag("NearbyPinDetails").e(e)
onePlaceBatch.add(Pair(i, updatedGroups[i]))
@ -457,7 +467,7 @@ class NearbyParentFragmentPresenter
if (bookmarkChangedPlacesBacklog.containsKey(group.place.location)) {
updatedGroups[index] = MarkerPlaceGroup(
bookmarkLocationDao
.findBookmarkLocation(updatedGroups[index].place),
.findBookmarkLocation(updatedGroups[index].place.name),
updatedGroups[index].place
)
}
@ -565,7 +575,7 @@ class NearbyParentFragmentPresenter
).sortedBy { it.getDistanceInDouble(mapFocus) }.take(NearbyController.MAX_RESULTS)
.map {
MarkerPlaceGroup(
bookmarkLocationDao.findBookmarkLocation(it), it
bookmarkLocationDao.findBookmarkLocation(it.name), it
)
}
ensureActive()

View file

@ -7,6 +7,8 @@ import android.database.MatrixCursor
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.RemoteException
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.argumentCaptor
@ -18,36 +20,20 @@ import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
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_COMMONS_LINK
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_DESCRIPTION
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_EXISTS
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_IMAGE_URL
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LABEL_ICON
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LANGUAGE
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LAT
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LONG
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_NAME
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_PIC
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.onCreate
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.onDelete
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.onUpdate
import fr.free.nrw.commons.db.AppDatabase
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 kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verifyNoInteractions
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -55,28 +41,11 @@ import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
class BookMarkLocationDaoTest {
private val columns =
arrayOf(
COLUMN_NAME,
COLUMN_LANGUAGE,
COLUMN_DESCRIPTION,
COLUMN_CATEGORY,
COLUMN_LABEL_TEXT,
COLUMN_LABEL_ICON,
COLUMN_IMAGE_URL,
COLUMN_WIKIPEDIA_LINK,
COLUMN_WIKIDATA_LINK,
COLUMN_COMMONS_LINK,
COLUMN_LAT,
COLUMN_LONG,
COLUMN_PIC,
COLUMN_EXISTS,
)
private val client: ContentProviderClient = mock()
private val database: SQLiteDatabase = mock()
private val captor = argumentCaptor<ContentValues>()
private lateinit var testObject: BookmarkLocationsDao
private lateinit var bookmarkLocationsDao: BookmarkLocationsDao
private lateinit var database: AppDatabase
private lateinit var examplePlaceBookmark: Place
private lateinit var exampleLabel: Label
private lateinit var exampleUri: Uri
@ -89,10 +58,18 @@ class BookMarkLocationDaoTest {
exampleUri = Uri.parse("wikimedia/uri")
exampleLocation = LatLng(40.0, 51.4, 1f)
builder = Sitelinks.Builder()
builder.setWikipediaLink("wikipediaLink")
builder.setWikidataLink("wikidataLink")
builder.setCommonsLink("commonsLink")
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).allowMainThreadQueries().build()
bookmarkLocationsDao = database.bookmarkLocationsDao()
builder = Sitelinks.Builder().apply {
setWikipediaLink("wikipediaLink")
setWikidataLink("wikidataLink")
setCommonsLink("commonsLink")
}
examplePlaceBookmark =
Place(
@ -106,236 +83,75 @@ class BookMarkLocationDaoTest {
"picName",
false,
)
testObject = BookmarkLocationsDao { client }
}
@After
fun tearDown() {
database.close()
}
@Test
fun createTable() {
onCreate(database)
verify(database).execSQL(CREATE_TABLE_STATEMENT)
fun testForAddAndGetAllBookmarkLocations() = runBlocking {
bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations())
val bookmarks = bookmarkLocationsDao.getAllBookmarksLocations()
assertEquals(1, bookmarks.size)
val retrievedBookmark = bookmarks.first()
assertEquals(examplePlaceBookmark.name, retrievedBookmark.locationName)
assertEquals(examplePlaceBookmark.language, retrievedBookmark.locationLanguage)
}
@Test
fun deleteTable() {
onDelete(database)
inOrder(database) {
verify(database).execSQL(DROP_TABLE_STATEMENT)
verify(database).execSQL(CREATE_TABLE_STATEMENT)
}
fun testFindBookmarkByNameForTrue() = runBlocking {
bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations())
val exists = bookmarkLocationsDao.findBookmarkLocation(examplePlaceBookmark.name)
assertTrue(exists)
}
@Test
fun createFromCursor() {
createCursor(1).let { cursor ->
cursor.moveToFirst()
testObject.fromCursor(cursor).let {
assertEquals("en", it.language)
assertEquals("placeName", it.name)
assertEquals(Label.FOREST, it.label)
assertEquals("placeDescription", it.longDescription)
assertEquals(40.0, it.location.latitude, 0.001)
assertEquals(51.4, it.location.longitude, 0.001)
assertEquals("placeCategory", it.category)
assertEquals(builder.build().wikipediaLink, it.siteLinks.wikipediaLink)
assertEquals(builder.build().wikidataLink, it.siteLinks.wikidataLink)
assertEquals(builder.build().commonsLink, it.siteLinks.commonsLink)
assertEquals("picName", it.pic)
assertEquals(false, it.exists)
}
}
fun testFindBookmarkByNameForFalse() = runBlocking {
bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations())
val exists = bookmarkLocationsDao.findBookmarkLocation("xyz")
assertFalse(exists)
}
@Test
fun getAllLocationBookmarks() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14))
fun testDeleteBookmark() = runBlocking {
val bookmarkLocation = examplePlaceBookmark.toBookmarksLocations()
bookmarkLocationsDao.addBookmarkLocation(bookmarkLocation)
var result = testObject.allBookmarksLocations
bookmarkLocationsDao.deleteBookmarkLocation(bookmarkLocation)
assertEquals(14, result.size)
}
@Test(expected = RuntimeException::class)
fun getAllLocationBookmarksTranslatesExceptions() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException(""))
testObject.allBookmarksLocations
val bookmarks = bookmarkLocationsDao.getAllBookmarksLocations()
assertTrue(bookmarks.isEmpty())
}
@Test
fun getAllLocationBookmarksReturnsEmptyList_emptyCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0))
assertTrue(testObject.allBookmarksLocations.isEmpty())
fun testUpdateBookmarkForTrue() = runBlocking {
val exists = bookmarkLocationsDao.updateBookmarkLocation(examplePlaceBookmark)
assertTrue(exists)
}
@Test
fun getAllLocationBookmarksReturnsEmptyList_nullCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null)
assertTrue(testObject.allBookmarksLocations.isEmpty())
fun testUpdateBookmarkForFalse() = runBlocking {
val newBookmark = examplePlaceBookmark.toBookmarksLocations()
bookmarkLocationsDao.addBookmarkLocation(newBookmark)
val exists = bookmarkLocationsDao.updateBookmarkLocation(examplePlaceBookmark)
assertFalse(exists)
}
@Test
fun cursorsAreClosedAfterGetAllLocationBookmarksQuery() {
val mockCursor: Cursor = mock()
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor)
whenever(mockCursor.moveToFirst()).thenReturn(false)
fun testGetAllBookmarksLocationsPlace() = runBlocking {
val bookmarkLocation = examplePlaceBookmark.toBookmarksLocations()
bookmarkLocationsDao.addBookmarkLocation(bookmarkLocation)
testObject.allBookmarksLocations
verify(mockCursor).close()
}
@Test
fun updateNewLocationBookmark() {
whenever(client.insert(any(), any())).thenReturn(Uri.EMPTY)
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null)
assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark))
verify(client).insert(eq(BASE_URI), captor.capture())
captor.firstValue.let { cv ->
assertEquals(13, cv.size())
assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME))
assertEquals(examplePlaceBookmark.language, cv.getAsString(COLUMN_LANGUAGE))
assertEquals(examplePlaceBookmark.longDescription, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(examplePlaceBookmark.label.text, cv.getAsString(COLUMN_LABEL_TEXT))
assertEquals(examplePlaceBookmark.category, cv.getAsString(COLUMN_CATEGORY))
assertEquals(examplePlaceBookmark.location.latitude, cv.getAsDouble(COLUMN_LAT), 0.001)
assertEquals(examplePlaceBookmark.location.longitude, cv.getAsDouble(COLUMN_LONG), 0.001)
assertEquals(examplePlaceBookmark.siteLinks.wikipediaLink.toString(), cv.getAsString(COLUMN_WIKIPEDIA_LINK))
assertEquals(examplePlaceBookmark.siteLinks.wikidataLink.toString(), cv.getAsString(COLUMN_WIKIDATA_LINK))
assertEquals(examplePlaceBookmark.siteLinks.commonsLink.toString(), cv.getAsString(COLUMN_COMMONS_LINK))
assertEquals(examplePlaceBookmark.pic, cv.getAsString(COLUMN_PIC))
assertEquals(examplePlaceBookmark.exists.toString(), cv.getAsString(COLUMN_EXISTS))
}
}
@Test
fun updateExistingLocationBookmark() {
whenever(client.delete(isA(), isNull(), isNull())).thenReturn(1)
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1))
assertFalse(testObject.updateBookmarkLocation(examplePlaceBookmark))
verify(client).delete(eq(BookmarkLocationsContentProvider.uriForName(examplePlaceBookmark.name)), isNull(), isNull())
}
@Test
fun findExistingLocationBookmark() {
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1))
assertTrue(testObject.findBookmarkLocation(examplePlaceBookmark))
}
@Test(expected = RuntimeException::class)
fun findLocationBookmarkTranslatesExceptions() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException(""))
testObject.findBookmarkLocation(examplePlaceBookmark)
}
@Test
fun findNotExistingLocationBookmarkReturnsNull_emptyCursor() {
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(0))
assertFalse(testObject.findBookmarkLocation(examplePlaceBookmark))
}
@Test
fun findNotExistingLocationBookmarkReturnsNull_nullCursor() {
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null)
assertFalse(testObject.findBookmarkLocation(examplePlaceBookmark))
}
@Test
fun cursorsAreClosedAfterFindLocationBookmarkQuery() {
val mockCursor: Cursor = mock()
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor)
whenever(mockCursor.moveToFirst()).thenReturn(false)
testObject.findBookmarkLocation(examplePlaceBookmark)
verify(mockCursor).close()
}
@Test
fun migrateTableVersionFrom_v1_to_v2() {
onUpdate(database, 1, 2)
// Table didn't exist before v5
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v2_to_v3() {
onUpdate(database, 2, 3)
// Table didn't exist before v5
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v3_to_v4() {
onUpdate(database, 3, 4)
// Table didnt exist before v5
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v4_to_v5() {
onUpdate(database, 4, 5)
// Table didnt change in version 5
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v5_to_v6() {
onUpdate(database, 5, 6)
// Table didnt change in version 6
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v6_to_v7() {
onUpdate(database, 6, 7)
// Table didnt change in version 7
verifyNoInteractions(database)
}
@Test
fun migrateTableVersionFrom_v7_to_v8() {
onUpdate(database, 7, 8)
verify(database).execSQL(CREATE_TABLE_STATEMENT)
}
@Test
fun migrateTableVersionFrom_v12_to_v13() {
onUpdate(database, 12, 13)
verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;")
}
@Test
fun migrateTableVersionFrom_v13_to_v14() {
onUpdate(database, 13, 14)
verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;")
}
@Test
fun migrateTableVersionFrom_v14_to_v15() {
onUpdate(database, 14, 15)
verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;")
}
private fun createCursor(rows: Int): Cursor =
MatrixCursor(columns, rows).apply {
repeat(rows) {
newRow().apply {
add("placeName")
add("en")
add("placeDescription")
add("placeCategory")
add(Label.FOREST.text)
add(Label.FOREST.icon)
add("placeImage")
add("wikipediaLink")
add("wikidataLink")
add("commonsLink")
add(40.0)
add(51.4)
add("picName")
add(false)
}
}
val bookmarks = bookmarkLocationsDao.getAllBookmarksLocationsPlace()
assertEquals(1, bookmarks.size)
assertEquals(examplePlaceBookmark.name, bookmarks.first().name)
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.bookmarks.locations
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.nearby.Place
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Before
import org.junit.Test
@ -19,10 +20,12 @@ class BookmarkLocationControllerTest {
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
whenever(bookmarkDao!!.allBookmarksLocations)
MockitoAnnotations.openMocks(this)
runBlocking {
whenever(bookmarkDao!!.getAllBookmarksLocationsPlace())
.thenReturn(mockBookmarkList)
}
}
/**
* Get mock bookmark list
@ -66,7 +69,7 @@ class BookmarkLocationControllerTest {
* Test case where all bookmark locations are fetched and media is found against it
*/
@Test
fun loadBookmarkedLocations() {
fun loadBookmarkedLocations() = runBlocking {
val bookmarkedLocations =
bookmarkLocationsController.loadFavoritesLocations()
Assert.assertEquals(2, bookmarkedLocations.size.toLong())

View file

@ -10,6 +10,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.OkHttpConnectionFactory
import fr.free.nrw.commons.R
@ -22,11 +23,14 @@ import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter
import fr.free.nrw.commons.profile.ProfileActivity
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
@ -129,12 +133,14 @@ class BookmarkLocationFragmentUnitTests {
*/
@Test
fun testInitNonEmpty() {
runBlocking {
whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList)
val method: Method =
BookmarkLocationsFragment::class.java.getDeclaredMethod("initList")
method.isAccessible = true
method.invoke(fragment)
}
}
/**
* test onCreateView
@ -168,7 +174,11 @@ class BookmarkLocationFragmentUnitTests {
*/
@Test
@Throws(Exception::class)
fun testOnResume() {
fragment.onResume()
fun testOnResume() = runBlocking {
val fragmentSpy = spy(fragment)
whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList)
fragmentSpy.onResume()
verify(fragmentSpy).initList()
}
}

View file

@ -8,6 +8,7 @@ import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@ -463,7 +464,9 @@ class NearbyParentFragmentPresenterTest {
nearbyPlacesInfo.searchLatLng = latestLocation
nearbyPlacesInfo.placeList = emptyList<Place>()
whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList())
runBlocking {
whenever(bookmarkLocationsDao.getAllBookmarksLocations()).thenReturn(Collections.emptyList())
}
nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null)
Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false)
}