diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 958c13fda..bcbef52fd 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,6 +1,11 @@ name: Android CI -on: [push, pull_request, workflow_dispatch] +on: [push, pull_request, workflow_dispatch] + +permissions: + pull-requests: write + contents: read + actions: read concurrency: group: build-${{ github.event.pull_request.number || github.ref }} @@ -102,3 +107,64 @@ 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 + 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}`); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 21dd14131..3b3b798eb 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -232,13 +232,23 @@ public class Place implements Parcelable { */ @Nullable public String getWikiDataEntityId() { + if (this.entityID != null && !this.entityID.equals("")) { + return this.entityID; + } + if (!hasWikidataLink()) { Timber.d("Wikidata entity ID is null for place with sitelink %s", siteLinks.toString()); return null; } + //Determine entityID from link String wikiDataLink = siteLinks.getWikidataLink().toString(); - return wikiDataLink.replace("http://www.wikidata.org/entity/", ""); + + if (wikiDataLink.contains("http://www.wikidata.org/entity/")) { + this.entityID = wikiDataLink.substring("http://www.wikidata.org/entity/".length()); + return this.entityID; + } + return null; } /** 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 dd3179463..bac774ba0 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 @@ -11,6 +11,7 @@ import android.content.IntentFilter import android.content.res.Configuration import android.graphics.Color import android.graphics.Paint +import android.graphics.drawable.Drawable import android.location.Location import android.location.LocationManager import android.net.Uri @@ -241,6 +242,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), @Volatile private var stopQuery = false + private var drawableCache: MutableMap, Drawable>? = null // Explore map data (for if we came from Explore) private var prevZoom = 0.0 @@ -747,6 +749,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), startMapWithoutPermission() } } + drawableCache = HashMap() } /** @@ -1527,7 +1530,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), marker.showInfoWindow() presenter!!.handlePinClicked(updatedPlace) savePlaceToDatabase(place) - val icon = ContextCompat.getDrawable( + val icon = getDrawable( requireContext(), getIconFor(updatedPlace, isBookMarked) ) @@ -2077,8 +2080,35 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), ) } + /** + * Gets the specified Drawable object. This is a wrapper method for ContextCompat.getDrawable(). + * This method caches results from previous calls for faster retrieval. + * + * @param context The context to use to get the Drawable + * @param id The integer that describes the Drawable resource + * @return The Drawable object + */ + private fun getDrawable(context: Context?, id: Int?): Drawable? { + if (drawableCache == null || context == null || id == null) { + return null + } + + val key = Pair(context, id) + if (!drawableCache!!.containsKey(key)) { + val drawable = ContextCompat.getDrawable(context, id) + + if (drawable != null) { + drawableCache!![key] = drawable + } else { + return null + } + } + + return drawableCache!![key] + } + fun convertToMarker(place: Place, isBookMarked: Boolean): Marker { - val icon = ContextCompat.getDrawable(requireContext(), getIconFor(place, isBookMarked)) + val icon = getDrawable(requireContext(), getIconFor(place, isBookMarked)) val point = GeoPoint(place.location.latitude, place.location.longitude) val marker = Marker(binding!!.map) marker.position = point @@ -2589,7 +2619,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), Marker.ANCHOR_BOTTOM ) startMarker.icon = - ContextCompat.getDrawable( + getDrawable( this.requireContext(), fr.free.nrw.commons.R.drawable.current_location_marker ) @@ -2647,7 +2677,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), Marker(binding?.map).apply { position = it setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) - icon = ContextCompat.getDrawable(context, R.drawable.current_location_marker) + icon = getDrawable(context, R.drawable.current_location_marker) title = "Your Location" textLabelFontSize = 24 overlays.add(this) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.kt index bd411c938..0340082ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/ResultTuple.kt @@ -15,7 +15,7 @@ class ResultTuple { } constructor() { - language = "" + language = "bug" // Basa Ugi language - TODO Respect the `Default description language` setting. type = "" value = "" } 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 ce268f7a3..ed84751b0 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 @@ -25,9 +25,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okhttp3.internal.wait import timber.log.Timber +import java.io.IOException import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy @@ -75,8 +78,8 @@ class NearbyParentFragmentPresenter * - **connnectionCount**: number of parallel requests */ private object LoadPlacesAsyncOptions { - const val BATCH_SIZE = 3 - const val CONNECTION_COUNT = 3 + const val BATCH_SIZE = 10 + const val CONNECTION_COUNT = 20 } private var schedulePlacesUpdateJob: Job? = null @@ -91,7 +94,7 @@ class NearbyParentFragmentPresenter private object SchedulePlacesUpdateOptions { var skippedCount = 0 const val SKIP_LIMIT = 3 - const val SKIP_DELAY_MS = 500L + const val SKIP_DELAY_MS = 100L } // used to tell the asynchronous place detail loading job that the places' bookmarked status @@ -379,13 +382,32 @@ class NearbyParentFragmentPresenter ) } catch (e: Exception) { Timber.tag("NearbyPinDetails").e(e) - collectResults.send(indices.map { Pair(it, updatedGroups[it]) }) + //HTTP request failed. Try individual places + for (i in indices) { + launch { + val onePlaceBatch = mutableListOf>() + try { + val fetchedPlace = nearbyController.getPlaces( + mutableListOf(updatedGroups[i].place) + ) + + onePlaceBatch.add(Pair(i, MarkerPlaceGroup( + bookmarkLocationDao.findBookmarkLocation( + fetchedPlace[0]),fetchedPlace[0]))) + } catch (e: Exception) { + Timber.tag("NearbyPinDetails").e(e) + onePlaceBatch.add(Pair(i, updatedGroups[i])) + } + collectResults.send(onePlaceBatch) + } + } } } } } var collectCount = 0 - for (resultList in collectResults) { + while (collectCount < indicesToUpdate.size) { + val resultList = collectResults.receive() for ((index, fetchedPlaceGroup) in resultList) { val existingPlace = updatedGroups[index].place val finalPlaceGroup = MarkerPlaceGroup( @@ -442,9 +464,7 @@ class NearbyParentFragmentPresenter } } schedulePlacesUpdate(updatedGroups) - if (++collectCount == totalBatches) { - break - } + collectCount += resultList.size } collectResults.close() } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 75db6ffc0..6d28085b2 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -469,7 +469,7 @@ class UploadWorker( contribution: Contribution, ) { val wikiDataPlace = contribution.wikidataPlace - if (wikiDataPlace != null && wikiDataPlace.imageValue == null) { + if (wikiDataPlace != null) { if (!contribution.hasInvalidLocation()) { var revisionID: Long? = null try { diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 7f2d9a8f1..010e440b1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -1,5 +1,6 @@