diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index bcbef52fd..bc8b03c9e 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -1,11 +1,6 @@
name: Android CI
-on: [push, pull_request, workflow_dispatch]
-
-permissions:
- pull-requests: write
- contents: read
- actions: read
+on: [push, pull_request, workflow_dispatch]
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
@@ -17,17 +12,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache packages
id: cache-packages
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -42,7 +37,7 @@ jobs:
- name: AVD cache
if: github.event_name != 'pull_request'
- uses: actions/cache@v3
+ uses: actions/cache@v4
id: avd-cache
with:
path: |
@@ -107,64 +102,13 @@ jobs:
with:
name: prodDebugAPK
path: app/build/outputs/apk/prod/debug/app-*.apk
-
- - name: Comment on PR with APK download links
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v6
+
+ - name: Create and PR number artifact
+ run: |
+ echo "{\"pr_number\": ${{ github.event.pull_request.number || 'null' }}}" > pr_number.json
+
+ - name: Upload PR number artifact
+ uses: actions/upload-artifact@v4
with:
- github-token: ${{secrets.GITHUB_TOKEN}}
- script: |
- try {
- const token = process.env.GITHUB_TOKEN;
- if (!token) {
- throw new Error('GITHUB_TOKEN is not set. Please check workflow permissions.');
- }
-
-
- const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: context.runId
- });
-
- if (!artifacts || artifacts.length === 0) {
- console.log('No artifacts found for this workflow run.');
- return;
- }
-
- const betaArtifact = artifacts.find(artifact => artifact.name === "betaDebugAPK");
- const prodArtifact = artifacts.find(artifact => artifact.name === "prodDebugAPK");
-
- if (!betaArtifact || !prodArtifact) {
- console.log('Could not find both Beta and Prod APK artifacts.');
- console.log('Available artifacts:', artifacts.map(a => a.name).join(', '));
- return;
- }
-
- const betaDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${context.runId}/artifacts/${betaArtifact.id}`;
- const prodDownloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/suites/${context.runId}/artifacts/${prodArtifact.id}`;
-
- const commentBody = `
- 📱 **APK for pull request is ready to see the changes** 📱
- - [Download Beta APK](${betaDownloadUrl})
- - [Download Prod APK](${prodDownloadUrl})
- `;
-
- await github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: commentBody
- });
-
- console.log('Successfully posted comment with APK download links');
- } catch (error) {
- console.error('Error in PR comment creation:', error);
- if (error.message.includes('GITHUB_TOKEN')) {
- core.setFailed('Missing or invalid GITHUB_TOKEN. Please check repository secrets configuration.');
- } else if (error.status === 403) {
- core.setFailed('Permission denied. Please check workflow permissions in repository settings.');
- } else {
- core.setFailed(`Workflow failed: ${error.message}`);
- }
- }
+ name: pr_number
+ path: ./pr_number.json
diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml
index 933d08e3e..8e1a26e15 100644
--- a/.github/workflows/build-beta.yml
+++ b/.github/workflows/build-beta.yml
@@ -8,9 +8,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: set up JDK 17
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
diff --git a/.github/workflows/comment_artifacts_on_PR.yml b/.github/workflows/comment_artifacts_on_PR.yml
new file mode 100644
index 000000000..ee4ae7c46
--- /dev/null
+++ b/.github/workflows/comment_artifacts_on_PR.yml
@@ -0,0 +1,96 @@
+name: Comment Artifacts on PR
+
+on:
+ workflow_run:
+ workflows: [ "Android CI" ]
+ types: [ completed ]
+
+permissions:
+ pull-requests: write
+ contents: read
+
+concurrency:
+ group: comment-${{ github.event.workflow_run.id }}
+ cancel-in-progress: true
+
+jobs:
+ comment:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
+ steps:
+ - name: Download and process artifacts
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ const runId = context.payload.workflow_run.id;
+
+ const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: runId,
+ });
+
+ const prNumberArtifact = allArtifacts.data.artifacts.find(artifact => artifact.name === "pr_number");
+ if (!prNumberArtifact) {
+ console.log("pr_number artifact not found.");
+ return;
+ }
+
+ const download = await github.rest.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: prNumberArtifact.id,
+ archive_format: 'zip',
+ });
+
+ fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
+ const { execSync } = require('child_process');
+ execSync('unzip -q pr_number.zip -d ./pr_number/');
+ fs.unlinkSync('pr_number.zip');
+
+ const prData = JSON.parse(fs.readFileSync('./pr_number/pr_number.json', 'utf8'));
+ const prNumber = prData.pr_number;
+
+ if (!prNumber || prNumber === 'null') {
+ console.log("No valid PR number found in pr_number.json. Skipping.");
+ return;
+ }
+
+ const artifactsToLink = allArtifacts.data.artifacts.filter(artifact => artifact.name !== "pr_number");
+ if (artifactsToLink.length === 0) {
+ console.log("No artifacts to link found.");
+ return;
+ }
+
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: Number(prNumber),
+ });
+
+ const oldComments = comments.data.filter(comment =>
+ comment.body.startsWith("✅ Generated APK variants!")
+ );
+ for (const comment of oldComments) {
+ await github.rest.issues.deleteComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: comment.id,
+ });
+ console.log(`Deleted old comment ID: ${comment.id}`);
+ };
+
+ const commentBody = `✅ Generated APK variants!\n` +
+ artifactsToLink.map(artifact => {
+ const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
+ return `- 🤖 [Download ${artifact.name}](${artifactUrl})`;
+ }).join('\n');
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: Number(prNumber),
+ body: commentBody
+ });
diff --git a/README.md b/README.md
index cefb267aa..0b31ff5be 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Wikimedia Commons Android app

-[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amaster)
+[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain)
[](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
[](https://codecov.io/gh/commons-app/apps-android-commons)
@@ -45,7 +45,7 @@ This software is open source, licensed under the [Apache License 2.0][10].
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[2]: https://commons-app.github.io/
-[3]: https://github.com/commons-app/apps-android-commons/issues
+[3]: https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+-label%3Adebated+label%3Abug+-label%3A%22low+priority%22+-label%3Aupstream
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
diff --git a/app/.attach_pid781771 b/app/.attach_pid781771
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/build.gradle b/app/build.gradle
index 2bde0d4f1..6890177e8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -175,8 +175,8 @@ dependencies {
testImplementation "androidx.work:work-testing:$work_version"
//Glide
- implementation 'com.github.bumptech.glide:glide:4.12.0'
- annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
@@ -212,8 +212,8 @@ android {
defaultConfig {
//applicationId 'fr.free.nrw.commons'
- versionCode 1043
- versionName '5.1.2'
+ versionCode 1046
+ versionName '5.1.3'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion 21
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fb776920e..d56a874b5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -232,12 +232,6 @@
android:exported="false"
android:label="@string/provider_bookmarks"
android:syncable="false" />
-
,
+ categories: List?,
+ filename: String?,
+ fallbackDescription: String?,
+ author: String?,
+ user: String?,
+ dateUploaded: Date? = Date(),
+ license: String? = null,
+ licenseUrl: String? = null,
+ imageUrl: String? = null,
+ thumbUrl: String? = null,
+ coordinates: LatLng? = null,
+ descriptions: Map = emptyMap(),
+ depictionIds: List = emptyList(),
+ categoriesHiddenStatus: Map = emptyMap()
+ ) : this(
+ pageId = UUID.randomUUID().toString(),
+ filename = filename,
+ fallbackDescription = fallbackDescription,
+ dateUploaded = dateUploaded,
+ author = author,
+ user = user,
+ categories = categories,
+ captions = captions,
+ license = license,
+ licenseUrl = licenseUrl,
+ imageUrl = imageUrl,
+ thumbUrl = thumbUrl,
+ coordinates = coordinates,
+ descriptions = descriptions,
+ depictionIds = depictionIds,
+ categoriesHiddenStatus = categoriesHiddenStatus
+ )
+
+ /**
+ * Returns Author if it's not null or empty, otherwise
+ * returns user
+ * @return Author or User
+ */
+ fun getAuthorOrUser(): String? {
+ return if (!author.isNullOrEmpty()) {
+ author
+ } else{
+ user
+ }
+ }
+
/**
* Gets media display title
* @return Media title
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java
deleted file mode 100644
index 8c9b559d4..000000000
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java
deleted file mode 100644
index 6e4c17c2e..000000000
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java
+++ /dev/null
@@ -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 loadFavoritesLocations() {
- return bookmarkLocationDao.getAllBookmarksLocations();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.kt
new file mode 100644
index 000000000..81ec80214
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.kt
@@ -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 =
+ bookmarkLocationDao.getAllBookmarksLocationsPlace()
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java
deleted file mode 100644
index fe4f603f4..000000000
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java
+++ /dev/null
@@ -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 clientProvider;
-
- @Inject
- public BookmarkLocationsDao(@Named("bookmarksLocation") Provider clientProvider) {
- this.clientProvider = clientProvider;
- }
-
- /**
- * Find all persisted locations bookmarks on database
- *
- * @return list of Place
- */
- @NonNull
- public List getAllBookmarksLocations() {
- List 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);
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt
new file mode 100644
index 000000000..2fa65b2d9
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt
@@ -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
+
+ /**
+ * 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 {
+ return getAllBookmarksLocations().map { it.toPlace() }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java
deleted file mode 100644
index f5ce556c4..000000000
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java
+++ /dev/null
@@ -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 cameraPickLauncherForResult =
- registerForActivityResult(new StartActivityForResult(),
- result -> {
- contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
- contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
- });
- });
-
- private final ActivityResultLauncher galleryPickLauncherForResult =
- registerForActivityResult(new StartActivityForResult(),
- result -> {
- contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
- contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
- });
- });
-
- private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback