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 ![GitHub issue custom search](https://img.shields.io/github/issues-search?label=%22good%20first%20issue%22%20issues&query=repo%3Acommons-app%2Fapps-android-commons%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22) -[![Build status](https://github.com/commons-app/apps-android-commons/actions/workflows/android.yml/badge.svg?branch=master)](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amaster) +[![Build status](https://github.com/commons-app/apps-android-commons/actions/workflows/android.yml/badge.svg?branch=main)](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain) [![Preview the app](https://img.shields.io/badge/Preview-Appetize.io-orange.svg)](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw) [![codecov](https://codecov.io/gh/commons-app/apps-android-commons/branch/master/graph/badge.svg)](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>() { - @Override - public void onActivityResult(Map 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 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt new file mode 100644 index 000000000..4ab21462c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt @@ -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> + 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 + if(view != null) { + viewLifecycleOwner.lifecycleScope.launch { + places = controller.loadFavoritesLocations() + updateUIList(places) + } + } + } + + private fun updateUIList(places: List) { + 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 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsViewModel.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsViewModel.kt new file mode 100644 index 000000000..b22723c0f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsViewModel.kt @@ -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 { +// return bookmarkLocationsDao.getAllBookmarksLocationsPlace() +// } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt new file mode 100644 index 000000000..66d670169 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt index 5dbcc59fd..0198c61a5 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt @@ -98,14 +98,9 @@ class GridViewAdapter( */ @SuppressLint("StringFormatInvalid") private fun setUploaderView(item: Media, uploader: TextView) { - if (!item.author.isNullOrEmpty()) { - uploader.visibility = View.VISIBLE - uploader.text = context.getString( - R.string.image_uploaded_by, - item.user - ) - } else { - uploader.visibility = View.GONE - } + uploader.text = context.getString( + R.string.image_uploaded_by, + item.getAuthorOrUser() + ) } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt index d1dbf4509..32028cfd2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt @@ -54,7 +54,7 @@ an upload might take a dozen seconds. */ this.contribution = contribution this.position = position binding.contributionTitle.text = contribution.media.mostRelevantCaption - binding.authorView.text = contribution.media.author + binding.authorView.text = contribution.media.getAuthorOrUser() //Removes flicker of loading image. binding.contributionImage.hierarchy.fadeDuration = 0 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt index 269536428..7027950e3 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt @@ -10,7 +10,7 @@ interface ContributionsContract { interface View { fun showMessage(localizedMessage: String) - fun getContext(): Context + fun getContext(): Context? } interface UserActionListener : BasePresenter { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt index 0b7736bab..f293ccc44 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt @@ -74,12 +74,9 @@ import java.util.Date import javax.inject.Inject import javax.inject.Named -class ContributionsFragment - - : CommonsDaggerSupportFragment(), FragmentManager.OnBackStackChangedListener, +class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.OnBackStackChangedListener, LocationUpdateListener, MediaDetailProvider, SensorEventListener, ICampaignsView, - ContributionsContract.View, - ContributionsListFragment.Callback { + ContributionsContract.View, ContributionsListFragment.Callback { @JvmField @Inject @Named("default_preferences") @@ -307,9 +304,11 @@ class ContributionsFragment } } notification.setOnClickListener { view: View? -> - startYourself( - context, "unread" - ) + context?.let { + startYourself( + it, "unread" + ) + } } } @@ -500,7 +499,7 @@ class ContributionsFragment private fun setUploadCount() { okHttpJsonApiClient - ?.getUploadCount((activity as MainActivity).sessionManager?.currentAccount!!.name) + ?.getUploadCount(sessionManager?.currentAccount!!.name) ?.subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread())?.let { compositeDisposable.add( @@ -889,14 +888,16 @@ class ContributionsFragment * this function updates the number of contributions */ fun upDateUploadCount() { - WorkManager.getInstance(context) - .getWorkInfosForUniqueWorkLiveData(UploadWorker::class.java.simpleName).observe( - viewLifecycleOwner - ) { workInfos: List -> - if (workInfos.size > 0) { - setUploadCount() + context?.let { + WorkManager.getInstance(it) + .getWorkInfosForUniqueWorkLiveData(UploadWorker::class.java.simpleName).observe( + viewLifecycleOwner + ) { workInfos: List -> + if (workInfos.size > 0) { + setUploadCount() + } } - } + } } @@ -953,7 +954,7 @@ class ContributionsFragment Timber.d("Skipping re-upload for non-failed %s", contribution.toString()) } } else { - showLongToast(context, R.string.this_function_needs_network_connection) + context?.let { showLongToast(it, R.string.this_function_needs_network_connection) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.kt index 617051e52..2a36101b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.kt @@ -80,9 +80,11 @@ class ContributionsPresenter @Inject internal constructor( .save(contribution) .subscribeOn(ioThreadScheduler) .subscribe { - makeOneTimeWorkRequest( - view!!.getContext().applicationContext, ExistingWorkPolicy.KEEP - ) + view!!.getContext()?.applicationContext?.let { + makeOneTimeWorkRequest( + it, ExistingWorkPolicy.KEEP + ) + } }) } } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt index 1377ae281..7cb7f60f7 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -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) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index 0c34bbdec..74ec9bc89 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -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 } diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt index be0b2bd79..09959d0ef 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt @@ -111,7 +111,7 @@ class DeleteHelper @Inject constructor( val userPageString = "\n{{subst:idw|${media.filename}}} ~~~~" - val creator = media.author + val creator = media.getAuthorOrUser() ?: throw RuntimeException("Failed to nominate for deletion") return pageEditClient.prependEdit( diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt index 58d9039d5..85af7f6eb 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -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() + } + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt index 5468cfa10..eb0ce83f3 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt @@ -63,9 +63,4 @@ abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInje return getInstance(activity.applicationContext) } - - // Ensure getContext() returns a non-null Context - override fun getContext(): Context { - return super.getContext() ?: throw IllegalStateException("Context is null") - } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt index 1882f77a9..80a0626cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt index de084ba50..0cfb270a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt @@ -39,7 +39,7 @@ class MediaConverter metadata.licenseShortName(), metadata.prefixedLicenseUrl, getAuthor(metadata), - getAuthor(metadata), + imageInfo.getUser(), MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()), metadata.latLng, entity.labels().mapValues { it.value.value() }, diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt index c987b76c2..364b5d363 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt @@ -52,12 +52,7 @@ class SearchImagesViewHolder( binding.categoryImageView.setOnClickListener { onImageClicked(item.second) } binding.categoryImageTitle.text = media.mostRelevantCaption binding.categoryImageView.setImageURI(media.thumbUrl) - if (media.author?.isNotEmpty() == true) { - binding.categoryImageAuthor.visibility = View.VISIBLE - binding.categoryImageAuthor.text = - containerView.context.getString(R.string.image_uploaded_by, media.user) - } else { - binding.categoryImageAuthor.visibility = View.GONE - } + binding.categoryImageAuthor.text = + containerView.context.getString(R.string.image_uploaded_by, media.getAuthorOrUser()) } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index 40c9785db..77ff1df0c 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -16,6 +16,7 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ArrayAdapter import android.widget.Button @@ -405,9 +406,14 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C * Gets the height of the frame layout as soon as the view is ready and updates aspect ratio * of the picture. */ - view.post { - frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight - updateAspectRatio(binding.mediaDetailScrollView.width) + view.post{ + val width = binding.mediaDetailScrollView.width + if (width > 0) { + frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight + updateAspectRatio(width) + } else { + view.postDelayed({ updateAspectRatio(binding.root.width) }, 1) + } } return view diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 545e96624..cba582a35 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -185,10 +185,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple * or a fragment */ private void initProvider() { - if (getParentFragment() != null) { + if (getParentFragment() instanceof MediaDetailProvider) { provider = (MediaDetailProvider) getParentFragment(); - } else { + } else if (getActivity() instanceof MediaDetailProvider) { provider = (MediaDetailProvider) getActivity(); + } else { + throw new ClassCastException("Parent must implement MediaDetailProvider"); } } @@ -326,7 +328,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple .append("\n\n"); builder.append("User that you want to report: ") - .append(media.getAuthor()) + .append(media.getUser()) .append("\n\n"); if (sessionManager.getUserName() != null) { @@ -421,7 +423,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple // Initialize bookmark object bookmark = new Bookmark( m.getFilename(), - m.getAuthor(), + m.getAuthorOrUser(), BookmarkPicturesContentProvider.uriForName(m.getFilename()) ); updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image)); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt index 8bcc21e40..a83d49f75 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt @@ -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 } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt new file mode 100644 index 000000000..236316fef --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt index 7156568b6..d9a76c25d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt @@ -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>, ActivityResultLauncher) -> 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,13 +99,15 @@ fun placeAdapterDelegate( GONE } - bookmarkButtonImage.setImageResource( - if (bookmarkLocationDao.findBookmarkLocation(item)) { - R.drawable.ic_round_star_filled_24px - } else { - R.drawable.ic_round_star_border_24px - }, - ) + scope?.launch { + bookmarkButtonImage.setImageResource( + if (bookmarkLocationDao.findBookmarkLocation(item.name)) { + R.drawable.ic_round_star_filled_24px + } else { + R.drawable.ic_round_star_border_24px + }, + ) + } } nearbyButtonLayout.directionsButton.setOnLongClickListener { onDirectionsLongPressed() } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java index e46e95353..1d59fcd34 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java @@ -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); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index d2e73441d..25baf3a92 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -39,11 +39,16 @@ import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.snackbar.Snackbar @@ -51,6 +56,7 @@ import com.jakewharton.rxbinding2.view.RxView import com.jakewharton.rxbinding3.appcompat.queryTextChanges import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.MapController.NearbyPlacesInfo +import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.Utils import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao @@ -67,6 +73,10 @@ import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermission import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType import fr.free.nrw.commons.location.LocationUpdateListener +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab import fr.free.nrw.commons.nearby.BottomSheetAdapter import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener import fr.free.nrw.commons.nearby.CheckBoxTriStates @@ -75,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 @@ -118,17 +129,26 @@ import timber.log.Timber import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Named +import javax.sql.DataSource import kotlin.concurrent.Volatile -class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmentContract.View, - WikidataP18EditListener, LocationUpdateListener, LocationPermissionCallback, ItemClickListener { +class NearbyParentFragment : CommonsDaggerSupportFragment(), + NearbyParentFragmentContract.View, + WikidataP18EditListener, + LocationUpdateListener, + LocationPermissionCallback, + ItemClickListener, + MediaDetailPagerFragment.MediaDetailProvider { var binding: FragmentNearbyParentBinding? = null val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver { @@ -163,6 +183,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen @Named("default_preferences") lateinit var applicationKvStore: JsonKvStore + @Inject + lateinit var mediaClient: MediaClient + + lateinit var mediaDetails: MediaDetailPagerFragment + + lateinit var media: Media + @Inject lateinit var bookmarkLocationDao: BookmarkLocationsDao @@ -196,7 +223,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen private var view: View? = null private var scope: LifecycleCoroutineScope? = null private var presenter: NearbyParentFragmentPresenter? = null - private var isDarkTheme = false + private var _isDarkTheme = false private var isFABsExpanded = false private var selectedPlace: Place? = null private var clickedMarker: Marker? = null @@ -434,7 +461,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } } } - isDarkTheme = systemThemeUtils?.isDeviceInNightMode() == true + _isDarkTheme = systemThemeUtils?.isDeviceInNightMode() == true if (Utils.isMonumentsEnabled(Date())) { binding?.rlContainerWlmMonthMessage?.visibility = View.VISIBLE } else { @@ -604,7 +631,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen * another refactor */ private fun initThemePreferences() { - if (isDarkTheme) { + if (_isDarkTheme) { binding!!.bottomSheetNearby.rvNearbyList.setBackgroundColor( requireContext().resources.getColor(fr.free.nrw.commons.R.color.contributionListDarkBackground) ) @@ -638,21 +665,23 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen 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 } @@ -716,6 +745,10 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen presenter?.attachView(this) registerNetworkReceiver() + binding?.coordinatorLayout?.visibility = View.VISIBLE + binding?.map?.setMultiTouchControls(true) + binding?.map?.isClickable = true + if (isResumed && (activity as? MainActivity)?.activeFragment == ActiveFragment.NEARBY) { if (activity?.let { locationPermissionsHelper?.checkLocationPermission(it) } == true) { locationPermissionGranted() @@ -882,7 +915,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } override fun isDarkTheme(): Boolean { - return isDarkTheme + return _isDarkTheme } }) binding!!.nearbyFilterList.root @@ -1853,7 +1886,31 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } fun backButtonClicked(): Boolean { - return presenter!!.backButtonClicked() + if (::mediaDetails.isInitialized && mediaDetails.isVisible) { + removeFragment(mediaDetails) + + binding?.coordinatorLayout?.visibility = View.VISIBLE + binding?.map?.setMultiTouchControls(true) + binding?.map?.isClickable = true + + val transaction = childFragmentManager.beginTransaction() + val fragmentContainer = childFragmentManager.findFragmentById(R.id.coordinator_layout) + + if (fragmentContainer != null) { + transaction.show(fragmentContainer) + } + + transaction.commit() + childFragmentManager.executePendingTransactions() + + (activity as? MainActivity)?.showTabs() + (activity as? MainActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(false) + return true + } else { + (activity as? MainActivity)?.setSelectedItemId(NavTab.NEARBY.code()) + } + + return presenter?.backButtonClicked() ?: false } override fun onLocationPermissionDenied(toastMessage: String) { @@ -2249,34 +2306,34 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen // TODO: Decide button text for fitting in the screen (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_round_star_border_24px, + R.drawable.ic_round_star_border_24px, "" ) ) (dataList as ArrayList).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).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).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).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) ) ) @@ -2284,7 +2341,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen if (selectedPlace!!.hasCommonsLink()) { (dataList as ArrayList).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) ) ) @@ -2299,7 +2356,61 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen bottomSheetAdapter!!.setClickListener(this) binding!!.bottomSheetDetails.bottomSheetRecyclerView.adapter = bottomSheetAdapter updateBookmarkButtonImage(selectedPlace!!) - binding!!.bottomSheetDetails.icon.setImageResource(selectedPlace!!.label.icon) + + selectedPlace?.pic?.substringAfterLast("/")?.takeIf { it.isNotEmpty() }?.let { imageName -> + Glide.with(binding!!.bottomSheetDetails.icon.context) + .clear(binding!!.bottomSheetDetails.icon) + + val loadingDrawable = ContextCompat.getDrawable( + binding!!.bottomSheetDetails.icon.context, + R.drawable.loading_icon + ) + val animation = AnimationUtils.loadAnimation( + binding!!.bottomSheetDetails.icon.context, + R.anim.rotate + ) + + Glide.with(binding!!.bottomSheetDetails.icon.context) + .load("https://commons.wikimedia.org/wiki/Special:Redirect/file/$imageName?width=25") + .placeholder(loadingDrawable) + .error(selectedPlace!!.label.icon) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + binding!!.bottomSheetDetails.icon.clearAnimation() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource, + isFirstResource: Boolean + ): Boolean { + binding!!.bottomSheetDetails.icon.clearAnimation() + return false + } + }) + .into(binding!!.bottomSheetDetails.icon) + + if (binding!!.bottomSheetDetails.icon.drawable != null && binding!!.bottomSheetDetails.icon.drawable.constantState == loadingDrawable?.constantState) { + binding!!.bottomSheetDetails.icon.startAnimation(animation) + } else { + binding!!.bottomSheetDetails.icon.clearAnimation() + } + + binding!!.bottomSheetDetails.icon.setOnClickListener { + handleMediaClick(imageName) + } + } ?: run { + binding!!.bottomSheetDetails.icon.setImageResource(selectedPlace!!.label.icon) + } + binding!!.bottomSheetDetails.title.text = selectedPlace!!.name binding!!.bottomSheetDetails.category.text = selectedPlace!!.distance // Remove label since it is double information @@ -2354,6 +2465,101 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } } + private fun handleMediaClick(imageName: String) { + val decodedImageName = URLDecoder.decode(imageName, StandardCharsets.UTF_8.toString()) + + mediaClient.getMedia("File:$decodedImageName") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ mediaResponse -> + if (mediaResponse != null) { + // Create a Media object from the response + media = Media( + pageId = mediaResponse.pageId ?: UUID.randomUUID().toString(), + thumbUrl = mediaResponse.thumbUrl, + imageUrl = mediaResponse.imageUrl, + filename = mediaResponse.filename, + fallbackDescription = mediaResponse.fallbackDescription, + dateUploaded = mediaResponse.dateUploaded, + license = mediaResponse.license, + licenseUrl = mediaResponse.licenseUrl, + author = mediaResponse.author, + user = mediaResponse.user, + categories = mediaResponse.categories, + coordinates = mediaResponse.coordinates, + captions = mediaResponse.captions ?: emptyMap(), + descriptions = mediaResponse.descriptions ?: emptyMap(), + depictionIds = mediaResponse.depictionIds ?: emptyList(), + categoriesHiddenStatus = mediaResponse.categoriesHiddenStatus ?: emptyMap() + ) + // Remove existing fragment before showing new details + if (::mediaDetails.isInitialized && mediaDetails.isAdded) { + removeFragment(mediaDetails) + } + showMediaDetails() + } else { + Timber.e("Fetched media is null for image: $decodedImageName") + } + }, { throwable -> + Timber.e(throwable, "Error fetching media for image: $decodedImageName") + }) + } + + private fun showMediaDetails() { + binding?.map?.setMultiTouchControls(false) + binding?.map?.isClickable = false + + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + + + val transaction = childFragmentManager.beginTransaction() + + val fragmentContainer = childFragmentManager.findFragmentById(R.id.coordinator_layout) + if (fragmentContainer != null) { + transaction.hide(fragmentContainer) + } + + // Replace instead of add to ensure new fragment is used + transaction.replace(R.id.coordinator_layout, mediaDetails, "MediaDetailFragmentTag") + transaction.addToBackStack("Nearby_Parent_Fragment_Tag").commit() + childFragmentManager.executePendingTransactions() + + (activity as? MainActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + + if (mediaDetails.isAdded) { + mediaDetails.showImage(0) + } else { + Timber.e("Error: MediaDetailPagerFragment is NOT added") + } + } + + override fun getMediaAtPosition(i: Int): Media? { + return media + } + + override fun getTotalMediaCount(): Int { + return 2 + } + + override fun getContributionStateAt(position: Int): Int? { + return null + } + + override fun refreshNominatedMedia(index: Int) { + if (this::mediaDetails.isInitialized && !binding?.map?.isClickable!! == true) { + removeFragment(mediaDetails) + showMediaDetails() + } + } + + private fun removeFragment(fragment: Fragment) { + childFragmentManager + .beginTransaction() + .remove(fragment) + .commit() + childFragmentManager.executePendingTransactions() + } + private fun storeSharedPrefs(selectedPlace: Place) { applicationKvStore!!.putJson(WikidataConstants.PLACE_OBJECT, selectedPlace) val place = @@ -2363,12 +2569,16 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } 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 - } - bottomSheetAdapter!!.updateBookmarkIcon(bookmarkIcon) + NearbyUtil.getBookmarkLocationExists( + bookmarkLocationDao, + place.getName(), + scope, + bottomSheetAdapter!! + ) + } + + private fun toggleBookmarkButtonImage() { + bottomSheetAdapter?.toggleBookmarkIcon() } override fun onAttach(context: Context) { @@ -2546,26 +2756,31 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen 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) @@ -2577,13 +2792,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen } } - 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) } @@ -2597,13 +2812,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), NearbyParentFragmen 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() diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt index e5cc92667..adb687014 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt @@ -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( placeAdapterDelegate( bookmarkLocationsDao, + scope, onPlaceClicked, commonPlaceClickActions.onCameraClicked(), commonPlaceClickActions.onCameraLongPressed(), diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt index ed84751b0..b4639b14a 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt @@ -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,25 +137,31 @@ 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) - bookmarkChangedPlaces.add(place) - val placeIndex = - NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location } - NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup( - nowBookmarked, - NearbyController.markerLabelList[placeIndex].place - ) - nearbyParentFragmentView.setFilterState() + var nowBookmarked: Boolean + scope?.launch { + nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place) + bookmarkChangedPlaces.add(place) + val placeIndex = + NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location } + NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup( + nowBookmarked, + NearbyController.markerLabelList[placeIndex].place + ) + 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() diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt index 05ed5f665..b19da15e6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt @@ -16,6 +16,7 @@ import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.AdapterView.OnItemClickListener +import android.widget.Button import android.widget.EditText import android.widget.FrameLayout import android.widget.ImageView @@ -330,6 +331,9 @@ class UploadMediaDetailAdapter : RecyclerView.Adapter(R.id.cancel_button) + .setOnClickListener { v: View? -> dialog.dismiss() } + editText.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = hideRecentLanguagesSection() diff --git a/app/src/main/res/anim/rotate.xml b/app/src/main/res/anim/rotate.xml new file mode 100644 index 000000000..8c42dc3e5 --- /dev/null +++ b/app/src/main/res/anim/rotate.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/loading_icon.xml b/app/src/main/res/drawable/loading_icon.xml new file mode 100644 index 000000000..babc0da6f --- /dev/null +++ b/app/src/main/res/drawable/loading_icon.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index cf48861d0..5ffda9f2e 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -3,6 +3,7 @@ * 1917 Ekim Devrimi * Envlh * Gambollar +* GolyatGeri * Gorizon * Gırd * Marmase @@ -199,7 +200,7 @@ Çım berze cı İzahat nêvineya Pela dosyay commonsi - Pbcey Wikidata + Pbcey Wikidayıt Meqaley Wikipedia Muhtemel problemê nê resımi Resım zehf tariyo. @@ -216,7 +217,7 @@ Ravêre Cı kewe Telimati - Wikidata + Wikidayıt Wikipediya Commons Rey bıdê @@ -246,7 +247,7 @@ Weçinaye Raya mobiliya biyo bar Xerita - Resım , Wikidata dê biyo obcey %1$s miyan! + Resım , Wikidayıt dê biyo obcey %1$s miyan! Wallpaper eyar kerê Wallpaper eyar biyo! Quiz diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 91f9652c4..7f742a319 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -136,8 +136,8 @@ परिचय विकिमीडिया कॉमन्स एप्प एक मुक्त स्रोत एप्प है जो कि विकिमीडिया समुदाय के अनुदानप्राप्तकर्ताओं व स्वयंसेवकों द्वारा निर्मित एवं प्रबंधित है। विकिमीडिया फॉऊण्डेशन इस एप्प के निर्माण, विकास व प्रबंधन में किसी प्रकार से भी संलग्न नहीं है। त्रुटि की सूचना और सुझावों के लिए <a href=\"%1$s\"> GitHub समस्या </a> बनाएं - <u>गोपनीयता नीति</u> - <u>श्रेय</u> + गोपनीयता नीति + श्रेय परिचय प्रतिक्रिया दें (ईमेल द्वारा) कोई ईमेल साधन स्थापित नहीं @@ -150,7 +150,7 @@ डाउनलोड डिफॉल्ट लाइसेन्स पिछले शीर्षक/विवरण का उपयोग करें - रात्रि मोड + थीम एट्रीब्यूशन-शेयरअलाइक 4.0 एट्रिब्यूशन 4.0 एट्रीब्यूशन-शेयरअलाइक 3.0 @@ -263,12 +263,12 @@ विकीपीडिया कॉमन्स <u>हमें रेट करें</u> - <u>अक्सर पूछे जाने वाले प्रश्न</u> + अक्सर पूछे जाने वाले प्रश्न प्रशिक्षण छोड़ें इंटरनेट उपलब्ध नहीं सूचनाएं लाने में त्रुटि कोई सूचनाएँ नहीं मिलीं - <u>अनुवाद</u> + अनुवाद भाषाएँ आगे बढ़ें रद्द करें @@ -284,7 +284,7 @@ हाल की खोजें हाल में खोजे गये प्रश्न श्रेणी लोड करते समय त्रुटि उत्पन्न हुई। - मीडिया + मीडिया श्रेणियाँ निर्वाचित नक्शा @@ -315,7 +315,7 @@ सांख्यिकी धन्यवाद प्राप्त किया निर्वाचित चित्र - स्तर + स्तर %d चित्र अपलोड हुआ चित्रों को वापस नहीं किया गया उपयोग हुए चित्र diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 79f266d8b..e977e56d5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -4,6 +4,7 @@ * Akmaie Ajam * Arifin.wijaya * Birusian +* Boesenbergia * DARMAS BUDI SANTOSO * Daud I.F. Argana * Fafau06 @@ -295,7 +296,7 @@ Galat penyampaian pemberitahuan Galat mengambil gambar untuk ditinjau. Tekan segarkan untuk coba lagi. Pemberitahuan tidak ditemukan - Terjemahkan + Pertalaghi Bahasa Pilih bahasa untuk terjemahan yang ingin Anda kirimkan Lanjutkan diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index a0be9be25..2ef0b7c0a 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -181,6 +181,7 @@ Neen Beschrëftung Titel + Motiven Beschreiwung Diskussioun Auteur diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1aaee6e9c..731ac6a32 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -406,10 +406,10 @@ Gebruik een aangepaste auteursnaam in plaats van uw gebruikersnaam tijdens het uploaden van foto\'s Aangepaste auteursnaam Bijdragen - Dichtbij + In de buurt Meldingen Meldingen (gelezen) - Meldingen dichtbij weergeven + Melding in de buurt weergeven Toon in-app-melding voor de dichtstbijzijnde plaats die foto\'s nodig heeft Lijst Toestemming om op te slaan @@ -615,7 +615,7 @@ Kies voor de beste resultaten de modus van hoge nauwkeurigheid. Locatie inschakelen? Schakel locatiediensten in zodat de app uw huidige locatie toont - In de Buurt heeft locatie nodig om correct te werken + ‘In de buurt’ heeft locatietoegang nodig om correct te werken Voor de verkenningskaart is locatietoestemming nodig om afbeeldingen in de buurt weer te geven U moet locatietoestemming geven om de locatie automatisch in te stellen. Heeft u deze twee foto\'s op dezelfde plek gemaakt? Wilt u de breedtegraad/lengtegraad van de afbeelding rechts gebruiken? @@ -657,7 +657,7 @@ Wekelijks Alle tijden Uploaden - In de Buurt + In de buurt Gebruikt Mijn ranking Beperkte verbindingsmodus ingeschakeld! @@ -724,7 +724,7 @@ Wijzig items Categorieën bewerken Geavanceerde opties - U kunt de zoekopdracht Dichtbij aanpassen. Als u fouten krijgt, kunt u opnieuw instellen en toepassen. + U kunt de zoekopdracht ‘In de buurt’ aanpassen. Als u fouten krijgt, kunt u opnieuw instellen en toepassen. Toepassen Opnieuw instellen Locatiegegevens helpen wiki-bewerkers om uw foto te vinden, waardoor deze veel nuttiger wordt.\nUw recente uploads hebben geen locatie.\nWe raden u aan om de locatie in de instellingen van uw camera-app in te schakelen.\nBedankt voor het uploaden! @@ -835,4 +835,10 @@ Account Account laten verdwijnen Waarschuwing verwijdering account + Verdwijnen is een <b>laatste redmiddel</b> en moet <b>alleen worden gebruikt als u voor altijd wilt stoppen met bewerken</b> en om zoveel mogelijk van uw voorgaande relaties te verbergen.<br/><br/>Accountverwijdering op Wikipedia gebeurt door uw accountnaam te wijzigen opdat anderen uw bijdragen niet meer kunnen herkennen. De procedure wordt accountverdwijning genoemd. <b>Door verdwijnen wordt geen volledige anonimiteit gegarandeerd en worden geen bijdragen aan de projecten verwijderd.</b> + Bijschrift + Bijschrift gekopieerd naar klembord + Gefeliciteerd, alle foto’s in dit album zijn ofwel geüpload ofwel gemarkeerd als ‘niet om te uploaden’. + Weergeven in Verkennen + Weergeven in ‘In de buurt’ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 11f917c3f..a017d11e0 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -10,8 +10,14 @@ --> ਕਾਮਨਜ਼ ਮਾਰਕਾ + ਹਵਾਲੇ ਕਰੋ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ + ਕੈਮਰੇ ਰਾਹੀਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ + ਤਸਵੀਰਾਂ ਰਾਹੀਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ + ਸੁਰਖੀ + ਭਾਸ਼ਾ ਦਾ ਵੇਰਵਾ + ਸੁਰਖੀ ਵੇਰਵਾ ਤਸਵੀਰ ਸਾਰੇ @@ -37,14 +43,19 @@ ਪੜਚੋਲ ਕਰੋ ਦਿੱਖ ਆਮ + ਸੁਝਾਅ + ਪਰਦੇਦਾਰੀ ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਪਸੰਦਾਂ + ਚੜ੍ਹਾਉਣਾ ਜਾਰੀ ਐ ਵਰਤੋਂਕਾਰ ਨਾਂ ਲੰਘ-ਸ਼ਬਦ ਦਾਖ਼ਲ ਹੋਵੋ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? + ਖਾਤਾ ਬਣਾਓ ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… + ਸੁਰਖੀਆਂ ਅਤੇ ਵੇਰਵੇ ਨਵਿਆਏ ਜਾ ਰਹੇ ਹਨ ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! @@ -53,11 +64,13 @@ ਅੱਪਲੋਡ ਸ਼ੁਰੂ ਹੋਇਆ! %1$s ਅੱਪਲੋਡ ਹੋ ਗਏ! ਆਪਣਾ ਅੱਪਲੋਡ ਵੇਖਣ ਲਈ ਥਪੇੜੋ - %1$s ਅੱਪਲੋਡ ਸ਼ੁਰੂ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ + ਫਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਐ: %s %1$s ਅੱਪਲੋਡ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ %1$s ਦਾ ਅੱਪਲੋਡ ਖ਼ਤਮ ਹੋ ਰਿਹਾ ਹੈ - %1$s ਦਾ ਅੱਪਲੋਡ ਫੇਲ੍ਹ ਹੋਇਆ + %1$s ਨੂੰ ਚੜ੍ਹਾਉਣ ਵਿੱਚ ਨਾਕਾਮ + %1$s ਚੜ੍ਹਾਉਣਾ ਰੋਕਿਆ ਗਿਆ ਵੇਖਣ ਲਈ ਥਪੇੜੋ + ਵੇਖਣ ਲਈ ਥਪੇੜੋ ਮੇਰੇ ਅੱਪਲੋਡ ਕਤਾਰ ਵਿਚ ਫੇਲ੍ਹ ਹੋਇਆ @@ -68,12 +81,17 @@ ਨੇੜੇ-ਤੇੜੇ ਮੇਰੇ ਅੱਪਲੋਡ ਕੜੀ ਦੀ ਨਕਲ ਕਰੋ + ਕੜੀ ਨੂੰ ਚੂੰਢੀ-ਤਖਤੀ ਉੱਤੇ ਨਕਲ ਕੀਤਾ ਗਿਆ ਐ ਸਾਂਝਾ ਕਰੋ + ਫਾਇਲ ਸਫ਼ਾ ਵੇਖੋ ਸੁਰਖੀ (ਲੋੜੀਂਦੀ) + ਕਿਰਪਾ ਕਰਕੇ ਇਸ ਫਾਈਲ ਲਈ ਇੱਕ ਸੁਰਖੀ ਦਿਓ ਵੇਰਵਾ + ਸੁਰਖੀ ਦਾਖ਼ਲ ਹੋਣ ਵਿੱਚ ਅਸਮਰੱਥ - ਨੈੱਟਵਰਕ ਫੇਲ੍ਹ ਹੋਇਆ ਹੈ ਬਹੁਤ ਸਾਰੀਆਂ ਅਸਫ਼ਲ ਕੋਸ਼ਿਸ਼ਾਂ। ਥੋੜ੍ਹੀ ਦੇਰ ਬਾਅਦ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ। ਅਫ਼ਸੋਸ, ਇਹ ਵਰਤੋਂਕਾਰ ਨੂੰ ਕਾਮਨਜ਼ ਤੇ ਰੋਕ ਲਾਈ ਗਈ ਹੈ। + ਤੁਹਾਨੂੰ ਆਪਣਾ ਦੋ-ਕਾਰਕ ਪ੍ਰਮਾਣੀਕਰਨ ਕੋਡ ਦੇਣਾ ਪਵੇਗਾ। ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਚੜ੍ਹਾਉ ਇਸ ਸੈੱਟ ਨੂੰ ਨਾਂ ਦਿਓ @@ -85,10 +103,12 @@ ਸੂਚੀ ਫ਼ਿਲਹਾਲ ਕੋਈ ਅੱਪਲੋਡ ਨਹੀਂ %1$s ਨਾਲ਼ ਮੇਲ ਖਾਂਦੀ ਕੋਈ ਸ਼੍ਰੇਣੀ ਨਹੀਂ ਲੱਭੀ + %1$s ਨਾਲ ਮੇਲ ਖਾਂਦੀਆਂ ਕੋਈ ਵਿਕੀਡਾਟਾ ਚੀਜ਼ਾਂ ਨਹੀਂ ਲੱਭਿਆਂ। ਆਪਣੀਆਂ ਤਸਵੀਰਾਂ ਨੂੰ ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਵਿਚ ਜ਼ਿਆਦਾ ਲੱਭਣਯੋਗ ਬਣਾਉਣ ਲਈ ਸ਼੍ਰੇਣੀਆਂ ਜੋੜੋ।\n\nਸ਼੍ਰੇਣੀਆਂ ਜੋੜਨ ਲਈ ਟਾਈਪ ਕਰਨ ਅਰੰਭ ਕਰੋ।\nਇਸ ਕਾਰਜ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰਨ ਲਈ ਇਹ ਸੁਨੇਹਾ ਥਪੇੜੋ (ਜਾਂ ਵਾਪਸੀ ਬਟਨ ਦਬਾਓ)। ਸ਼੍ਰੇਣੀਆਂ ਪਸੰਦਾਂ ਖਾਤਾ ਬਣਾਓ + ਵਿਸ਼ੇਸ਼ ਤਸਵੀਰ ਸ਼੍ਰੇਣੀ ਇਸ ਬਾਰੇ ਅਜ਼ਾਦ ਸਰੋਤ ਸਾਫ਼ਟਵੇਅਰ ਨੂੰ <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a> ਅਧੀਨ ਜਾਰੀ ਕੀਤਾ ਗਿਆ ਹੈ @@ -105,9 +125,9 @@ ਰੱਦ ਕਰੋ ਇਹ ਤਸਵੀਰ ਅੱਪਲੋਡ ਕਰਨ ਨਾਲ ਹੀ ਮੈਂ ਦਾਅਵਾ ਕਰਦਾ ਹਾਂ/ਕਰਦੀ ਹਾਂ ਕਿ ਇਹ ਮੇਰਾ ਆਪਣਾ ਕਾਰਜ ਹੈ, ਕਿ ਇਸ ਤਹਿਤ ਕੋਈ ਕਾਪੀਰਾਈਟ ਉਲੰਘਣਾ ਨਹੀਂ ਕੀਤੀ ਗਈ ਅਤੇ <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਨੀਤੀਆਂ</a> ਮੁਤਾਬਿਕ ਇਹ ਠੀਕ ਹੈ। ਡਾਊਨਲੋਡ - ਲਸੰਸ - ਪਹਿਲਾਂ ਵਾਲਾ ਸਿਰਲੇਖ/ਜਾਣਕਾਰੀ ਵਰਤੋ - ਰਾਤ ਦਾ ਅੰਦਾਜ਼ + ਮੂਲ ਲਸੰਸ + ਪਿਛਲੇ ਸਿਰਲੇਖ ਅਤੇ ਵੇਰਵੇ ਦੀ ਵਰਤੋਂ ਕਰੋ + ਵਿਸ਼ਾ-ਵਸਤੂ Attribution-ShareAlike 4.0 Attribution 4.0 CC Attribution-ShareAlike 3.0 @@ -120,8 +140,14 @@ ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਜ਼ਿਆਦਾਤਰ ਉਹ ਤਸਵੀਰਾਂ ਦਾ ਭੰਡਾਰ ਹੈ ਜੋ ਵਿਕੀਪੀਡੀਆ \'ਤੇ ਵਰਤੀਆਂ ਜਾਂਦੀਆਂ ਹਨ। ਤੁਹਾਡੀਆਂ ਤਸਵੀਰਾਂ ਵਿਸ਼ਵ ਦੇ ਬਾਕੀ ਲੋਕਾਂ ਨੂੰ ਸਿੱਖਿਅਤ ਕਰਨ ਲਈ ਸਹਾਈ ਹਨ! ਕਿਰਪਾ ਕਰਕੇ ਉਹ ਤਸਵੀਰਾਂ ਨੂੰ ਚੜ੍ਹਾਉ ਜੋ ਤੁਹਾਡੇ ਵੱਲੋਂ ਲਈਆਂ ਗਈਆਂ ਹਨ ਜਾਂ ਬਣਾਈਆਂ ਗਈਆਂ ਹਨ: + ਕੁਦਰਤੀ ਵਸਤੂਆਂ (ਫੁੱਲ, ਜਾਨਵਰ, ਪਹਾੜ) + ਲਾਹੇਵੰਦ ਵਸਤੂਆਂ (ਸੈਕਲ, ਰੇਲ ਅੱਡਾ) + ਮਸ਼ਹੂਰ ਲੋਕ (ਤੁਹਾਡੇ mayor, ਓਲੰਪਿਕ ਖਿਡਾਰੀ ਜਿਨ੍ਹਾਂ ਨੂੰ ਤੁਸੀਂ ਮਿਲੇ ਸੀ) ਕਿਰਪਾ ਕਰਕੇ ਅਪਲੋਡ ਨਾ ਕਰੋ: + ਤੁਹਾਡੇ ਦੋਸਤਾਂ ਦੀਆਂ ਸੈਲਫ਼ੀਆਂ ਜਾਂ ਤਸਵੀਰਾਂ ਉਦਾਹਰਣ ਵਜੋਂ ਇਹ ਅਪਲੋਡ: + ਸਿਰਲੇਖ: ਸਿਡਨੀ ਓਪੇਰਾ ਹਾਊਸ + ਵੇਰਵਾ: ਸਿਡਨੀ ਓਪੇਰਾ ਹਾਊਸ ਜਿਵੇਂ ਖਾੜੀ ਦੇ ਪਾਰ ਤੋਂ ਦਿਖਦਾ ਐ ਆਪਣੀਆਂ ਤਸਵੀਰਾਂ ਦਾ ਯੋਗਦਾਨ ਪਾਓ। ਵਿਕੀਪੀਡੀਆ ਲੇਖਾਂ ਨੂੰ ਸੁਰਜੀਤ ਕਰ ਦਿਓ! ਵਿਕੀਪੀਡੀਆ ਉਤਲੀਆਂ ਤਸਵੀਰਾਂ ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਤੋਂ ਆਉਂਦੀਆਂ ਹਨ ਤੁਹਾਡੀਆਂ ਤਸਵੀਰਾਂ ਦੁਨੀਆਂ ਭਰ ਦੇ ਲੋਕਾਂ ਨੂੰ ਪੜ੍ਹਨ ਵਿਚ ਮਦਦ ਕਰਦੀਆਂ ਹਨ। @@ -132,6 +158,7 @@ ਸ਼੍ਰੇਣੀਆਂ ਲੱਦ ਰਿਹਾ ਹੈ... ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ + ਕੋਈ ਸੁਰਖੀ ਨਹੀਂ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ ਅਣਜਾਣ ਲਸੰਸ @@ -139,8 +166,10 @@ ਆਗਿਆ ਚਾਹੀਦੀ ਹੈ: ਬਾਹਰੀ ਸਟੋਰੇਜ ਬਾਰੇ। ਇਸ ਤੋਂ ਬਿਨਾਂ ਐਪ ਕਾਰਜ ਨਹੀਂ ਕਰ ਸਕੇਗੀ। ਠੀਕ ਹੈ ਖ਼ਬਰਦਾਰ + ਚੜ੍ਹਾਉ ਹਾਂ ਨਹੀਂ + ਸੁਰਖੀ ਸਿਰਲੇਖ ਵੇਰਵਾ ਗੱਲਬਾਤ diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 8c9e91ca5..0fc6a3dc0 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -56,7 +56,7 @@ امستنې خونديځ ته راپورته کول راپورته کول جريان لري - کارننوم + کارن‌نوم پټنوم خپل خونديځ بېټا ګڼون ته ورننوځئ ننوتل diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 9417f90ec..7b521a8ac 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -3,6 +3,7 @@ * A100Star * Ajeje Brazorf * Amire80 +* Annick green * Cabal * Googology * LeGuyanaisPure @@ -27,6 +28,7 @@ {{Identical|Submit}} {{identical|All}} + Reba ishakiro Status text about number of uploads left.\n* %1$d represents number of uploads left, including current one See the current issue [https://phabricator.wikimedia.org/T267142 T267142] tracked in Phabricator about the <code><nowiki>|zero=</nowiki></code> option currently not supported on Translatewiki.net with the custom <code><nowiki>{{PLURAL}}</nowiki></code> rules used by this project for Android, using a non-MediaWiki syntax. {{Identical|Upload}} @@ -190,6 +192,7 @@ {{Doc-commons-app-depicts}} {{identical|Bookmark}} Option to make the app\'s theme follow the global system setting. + ‘Nearby’ should be translated as in {{msg-wm|Commons-android-strings-navigation item nearby}} {{Identical|More}} {{Optional}}\n<code>&amp;#169;</code> is the copyright symbol (©). {{Doc-commons-app-depicts}} @@ -202,9 +205,12 @@ {{identical|Done}} {{Doc-commons-app-depicts}} {{Identical|Advanced options}} + ‘Nearby’ should be translated as in {{msg-wm|Commons-android-strings-navigation item nearby}} {{Identical|Detail}} \"Set as avatar\" should be translated the same as {{msg-wm|Commons-android-strings-menu set avatar}}. {{Doc-commons-app-depicts}} An answer to the question in {{msg-wm|Commons-android-strings-custom selector confirm deletion message}}. {{optional}} + “Explore” should be translated as in {{msg-wm|Commons-android-strings-navigation item explore}} + ‘Nearby’ should be translated as in {{msg-wm|Commons-android-strings-navigation item nearby}} diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 802a1e350..d25d949ae 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -876,6 +876,7 @@ Учётная запись Удалить учётную запись Предупреждение об удалении учётной записи + Удаление — это <b>крайняя мера</b>, и её следует <b>использовать только в том случае, если вы хотите навсегда прекратить редактирование</b>, а также скрыть как можно больше связанных с вами действий.<br/><br/> Удаление вашей учётной записи на Викискладе осуществляется путём изменения её имени, чтобы другие не могли определить ваши действия. <b>Удаление не гарантирует полной анонимности или удаления вклада в проектах</b>. Подпись Подпись скопирована в буфер обмена Поздравляем, все фотографии в этом альбоме либо загружены, либо помечены как не предназначенные для загрузки. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 371744b44..2fde6f007 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -821,4 +821,6 @@ Bildtext Bildtext kopierades till urklipp Grattis! Alla bilder i detta album har antingen laddats upp eller markerats för att inte laddas upp. + Visa i \"Utforska\" + Visa i \"I närheten\" diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index fbbe499d2..f3eb3562f 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -66,7 +66,7 @@ %s ಅಪ್ಲೋಡ್ ಸುರು ಆವೊಂದುಂಡು %1$s ಅಪ್ಲೋಡ್ ಆವೊಂದುಂಡು %1$s ಅಪ್ಲೋಡ್ ಕೈದ್ ಆವೊಂದುಂಡು. - %1$s ಅಪ್ಲೋಡ್ ಸರಿ ಆತಿಜಿ + %1$s ಅಪ್ಲೋಡ್ ಆತಿಜಿ ತುಯಾರ ಮೆಲ್ಲ ಒತ್ತುಲೆ ಎನ್ನ ದಿಂಜಯೀನಾ ವಿಚಾರೊಳು ದಿಂಜೊಂತುಂಡು diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt index 391dd8d17..4808d8518 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt @@ -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() - 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() + val bookmarks = bookmarkLocationsDao.getAllBookmarksLocationsPlace() + assertEquals(1, bookmarks.size) + assertEquals(examplePlaceBookmark.name, bookmarks.first().name) } - - @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) - } - } - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt index 3fd21c25f..a7afc6b14 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt @@ -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,9 +20,11 @@ class BookmarkLocationControllerTest { @Before fun setup() { - MockitoAnnotations.initMocks(this) - whenever(bookmarkDao!!.allBookmarksLocations) - .thenReturn(mockBookmarkList) + MockitoAnnotations.openMocks(this) + runBlocking { + whenever(bookmarkDao!!.getAllBookmarksLocationsPlace()) + .thenReturn(mockBookmarkList) + } } /** @@ -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()) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt index 24693dc85..52121bf84 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt @@ -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,11 +133,13 @@ class BookmarkLocationFragmentUnitTests { */ @Test fun testInitNonEmpty() { - whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList) - val method: Method = - BookmarkLocationsFragment::class.java.getDeclaredMethod("initList") - method.isAccessible = true - method.invoke(fragment) + runBlocking { + whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList) + val method: Method = + BookmarkLocationsFragment::class.java.getDeclaredMethod("initList") + method.isAccessible = true + method.invoke(fragment) + } } /** @@ -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() } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index 80cced5c9..230099810 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -96,7 +96,7 @@ class DeleteHelperTest { ).thenReturn("Media successfully deleted: Test Media Title") val creatorName = "Creator" - whenever(media.author).thenReturn("$creatorName") + whenever(media.getAuthorOrUser()).thenReturn("$creatorName") whenever(media.filename).thenReturn("Test file.jpg") val makeDeletion = deleteHelper.makeDeletion( context, @@ -133,7 +133,7 @@ class DeleteHelperTest { whenever(media.displayTitle).thenReturn("Test file") whenever(media.filename).thenReturn("Test file.jpg") - whenever(media.author).thenReturn("Creator (page does not exist)") + whenever(media.getAuthorOrUser()).thenReturn("Creator (page does not exist)") deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet() } @@ -148,7 +148,7 @@ class DeleteHelperTest { .thenReturn(Observable.just(false)) whenever(media.displayTitle).thenReturn("Test file") whenever(media.filename).thenReturn("Test file.jpg") - whenever(media.author).thenReturn("Creator (page does not exist)") + whenever(media.getAuthorOrUser()).thenReturn("Creator (page does not exist)") deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet() } @@ -163,7 +163,7 @@ class DeleteHelperTest { .thenReturn(Observable.just(true)) whenever(media.displayTitle).thenReturn("Test file") whenever(media.filename).thenReturn("Test file.jpg") - whenever(media.author).thenReturn("Creator (page does not exist)") + whenever(media.getAuthorOrUser()).thenReturn("Creator (page does not exist)") deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet() } @@ -221,7 +221,7 @@ class DeleteHelperTest { whenever(media.displayTitle).thenReturn("Test file") whenever(media.filename).thenReturn("Test file.jpg") - whenever(media.author).thenReturn(null) + whenever(media.getAuthorOrUser()).thenReturn(null) deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt index 80e9a53f9..96098e2a0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt @@ -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() - whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList()) + runBlocking { + whenever(bookmarkLocationsDao.getAllBookmarksLocations()).thenReturn(Collections.emptyList()) + } nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null) Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false) }