extraMap = new HashMap<>(4);
- extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
- extraMap
- .put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
- extraMap
- .put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
- extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
- return extraMap;
- }
-
- protected void fetchWithRequest(
- final OkHttpNetworkFetchState fetchState,
- final NetworkFetcher.Callback callback,
- final Request request) {
- final Call call = mCallFactory.newCall(request);
-
- fetchState
- .getContext()
- .addCallbacks(
- new BaseProducerContextCallbacks() {
- @Override
- public void onCancellationRequested() {
- onFetchCancellationRequested(call);
- }
- });
-
- call.enqueue(
- new okhttp3.Callback() {
- @Override
- public void onResponse(final Call call, final Response response) {
- onFetchResponse(fetchState, call, response, callback);
- }
-
- @Override
- public void onFailure(final Call call, final IOException e) {
- handleException(call, e, callback);
- }
- });
- }
-
- private void onFetchCancellationRequested(final Call call) {
- if (Looper.myLooper() != Looper.getMainLooper()) {
- call.cancel();
- } else {
- mCancellationExecutor.execute(call::cancel);
- }
- }
-
- private void onFetchResponse(final OkHttpNetworkFetchState fetchState, final Call call,
- final Response response,
- final NetworkFetcher.Callback callback) {
- fetchState.responseTime = SystemClock.elapsedRealtime();
- try (final ResponseBody body = response.body()) {
- if (!response.isSuccessful()) {
- handleException(
- call, new IOException("Unexpected HTTP code " + response),
- callback);
- return;
- }
-
- final BytesRange responseRange =
- BytesRange.fromContentRangeHeader(response.header("Content-Range"));
- if (responseRange != null
- && !(responseRange.from == 0
- && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
- // Only treat as a partial image if the range is not all of the content
- fetchState.setResponseBytesRange(responseRange);
- fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
- }
-
- long contentLength = body.contentLength();
- if (contentLength < 0) {
- contentLength = 0;
- }
- callback.onResponse(body.byteStream(), (int) contentLength);
- } catch (final Exception e) {
- handleException(call, e, callback);
- }
- }
-
- /**
- * Handles exceptions.
- *
- * OkHttp notifies callers of cancellations via an IOException. If IOException is caught
- * after request cancellation, then the exception is interpreted as successful cancellation and
- * onCancellation is called. Otherwise onFailure is called.
- */
- private void handleException(final Call call, final Exception e, final Callback callback) {
- if (call.isCanceled()) {
- callback.onCancellation();
- } else {
- callback.onFailure(e);
- }
- }
-
- public static class OkHttpNetworkFetchState extends FetchState {
-
- public long submitTime;
- public long responseTime;
- public long fetchCompleteTime;
-
- public OkHttpNetworkFetchState(
- final Consumer consumer, final ProducerContext producerContext) {
- super(consumer, producerContext);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt
new file mode 100644
index 000000000..c8de4022b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt
@@ -0,0 +1,199 @@
+package fr.free.nrw.commons.media
+
+import android.os.Looper
+import android.os.SystemClock
+import com.facebook.imagepipeline.common.BytesRange
+import com.facebook.imagepipeline.image.EncodedImage
+import com.facebook.imagepipeline.producers.BaseNetworkFetcher
+import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks
+import com.facebook.imagepipeline.producers.Consumer
+import com.facebook.imagepipeline.producers.FetchState
+import com.facebook.imagepipeline.producers.NetworkFetcher
+import com.facebook.imagepipeline.producers.ProducerContext
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import okhttp3.CacheControl
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import timber.log.Timber
+import java.io.IOException
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
+// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
+@Singleton
+class CustomOkHttpNetworkFetcher
+@JvmOverloads constructor(
+ private val mCallFactory: Call.Factory,
+ private val mCancellationExecutor: Executor,
+ private val defaultKvStore: JsonKvStore,
+ disableOkHttpCache: Boolean = true
+) : BaseNetworkFetcher() {
+
+ private val mCacheControl =
+ if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null
+ private val isLimitedConnectionMode: Boolean
+ get() = defaultKvStore.getBoolean(
+ CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
+ false
+ )
+
+ /**
+ * @param okHttpClient client to use
+ */
+ @Inject
+ constructor(
+ okHttpClient: OkHttpClient,
+ @Named("default_preferences") defaultKvStore: JsonKvStore
+ ) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore)
+
+ /**
+ * @param mCallFactory custom [Call.Factory] for fetching image from the network
+ * @param mCancellationExecutor executor on which fetching cancellation is performed if
+ * cancellation is requested from the UI Thread
+ * @param disableOkHttpCache true if network requests should not be cached by OkHttp
+ */
+ override fun createFetchState(consumer: Consumer, context: ProducerContext) =
+ OkHttpNetworkFetchState(consumer, context)
+
+ override fun fetch(
+ fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback
+ ) {
+ fetchState.submitTime = SystemClock.elapsedRealtime()
+
+ try {
+ if (isLimitedConnectionMode) {
+ Timber.d("Skipping loading of image as limited connection mode is enabled")
+ callback.onFailure(Exception("Failing image request as limited connection mode is enabled"))
+ return
+ }
+
+ val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get()
+
+ if (mCacheControl != null) {
+ requestBuilder.cacheControl(mCacheControl)
+ }
+
+ val bytesRange = fetchState.context.imageRequest.bytesRange
+ if (bytesRange != null) {
+ requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue())
+ }
+
+ fetchWithRequest(fetchState, callback, requestBuilder.build())
+ } catch (e: Exception) {
+ // handle error while creating the request
+ callback.onFailure(e)
+ }
+ }
+
+ override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) {
+ fetchState.fetchCompleteTime = SystemClock.elapsedRealtime()
+ }
+
+ override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) =
+ fetchState.toExtraMap(byteSize)
+
+ private fun fetchWithRequest(
+ fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request
+ ) {
+ val call = mCallFactory.newCall(request)
+
+ fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() {
+ override fun onCancellationRequested() {
+ onFetchCancellationRequested(call)
+ }
+ })
+
+ call.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) =
+ onFetchResponse(fetchState, call, response, callback)
+
+ override fun onFailure(call: Call, e: IOException) =
+ handleException(call, e, callback)
+ })
+ }
+
+ private fun onFetchCancellationRequested(call: Call) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ call.cancel()
+ } else {
+ mCancellationExecutor.execute { call.cancel() }
+ }
+ }
+
+ private fun onFetchResponse(
+ fetchState: OkHttpNetworkFetchState,
+ call: Call,
+ response: Response,
+ callback: NetworkFetcher.Callback
+ ) {
+ fetchState.responseTime = SystemClock.elapsedRealtime()
+ try {
+ response.body.use { body ->
+ if (!response.isSuccessful) {
+ handleException(call, IOException("Unexpected HTTP code $response"), callback)
+ return
+ }
+ val responseRange =
+ BytesRange.fromContentRangeHeader(response.header("Content-Range"))
+ if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
+ // Only treat as a partial image if the range is not all of the content
+ fetchState.responseBytesRange = responseRange
+ fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT
+ }
+
+ var contentLength = body!!.contentLength()
+ if (contentLength < 0) {
+ contentLength = 0
+ }
+ callback.onResponse(body.byteStream(), contentLength.toInt())
+ }
+ } catch (e: Exception) {
+ handleException(call, e, callback)
+ }
+ }
+
+ /**
+ * Handles exceptions.
+ *
+ * OkHttp notifies callers of cancellations via an IOException. If IOException is caught
+ * after request cancellation, then the exception is interpreted as successful cancellation and
+ * onCancellation is called. Otherwise onFailure is called.
+ */
+ private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) {
+ if (call.isCanceled()) {
+ callback.onCancellation()
+ } else {
+ callback.onFailure(e)
+ }
+ }
+}
+
+class OkHttpNetworkFetchState(
+ consumer: Consumer?, producerContext: ProducerContext?
+) : FetchState(consumer, producerContext) {
+ var submitTime: Long = 0
+ var responseTime: Long = 0
+ var fetchCompleteTime: Long = 0
+
+ fun toExtraMap(byteSize: Int) = buildMap {
+ put(QUEUE_TIME, (responseTime - submitTime).toString())
+ put(FETCH_TIME, (fetchCompleteTime - responseTime).toString())
+ put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString())
+ put(IMAGE_SIZE, byteSize.toString())
+ }
+
+ companion object {
+ private const val QUEUE_TIME = "queue_time"
+ private const val FETCH_TIME = "fetch_time"
+ private const val TOTAL_TIME = "total_time"
+ private const val IMAGE_SIZE = "image_size"
+ }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt
deleted file mode 100644
index fe96eb8cb..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package fr.free.nrw.commons.media
-
-data class IdAndCaptions(
- val id: String,
- val captions: Map,
-)
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
new file mode 100644
index 000000000..c989ee7e3
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.media
+
+data class IdAndLabels(
+ val id: String,
+ val labels: Map,
+) {
+ // if a label is available in user's locale, return it
+ // if not then check for english, else show any available.
+ fun getLocalizedLabel(locale: String): String? {
+ if (labels[locale] != null) {
+ return labels[locale]
+ }
+ if (labels["en"] != null) {
+ return labels["en"]
+ }
+ return labels.values.firstOrNull() ?: id
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt
new file mode 100644
index 000000000..ccc176154
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt
@@ -0,0 +1,76 @@
+package fr.free.nrw.commons.media
+
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentStatePagerAdapter
+import fr.free.nrw.commons.media.MediaDetailFragment.Companion.forMedia
+import timber.log.Timber
+
+// FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
+class MediaDetailAdapter(
+ val mediaDetailPagerFragment: MediaDetailPagerFragment,
+ fm: FragmentManager
+) : FragmentStatePagerAdapter(fm) {
+ /**
+ * Keeps track of the current displayed fragment.
+ */
+ private var currentFragment: Fragment? = null
+
+ override fun getItem(i: Int): Fragment {
+ if (i == 0) {
+ // See bug https://code.google.com/p/android/issues/detail?id=27526
+ if (mediaDetailPagerFragment.activity == null) {
+ Timber.d("Skipping getItem. Returning as activity is destroyed!")
+ return Fragment()
+ }
+ mediaDetailPagerFragment.binding!!.mediaDetailsPager.postDelayed(
+ { mediaDetailPagerFragment.requireActivity().invalidateOptionsMenu() }, 5
+ )
+ }
+ return if (mediaDetailPagerFragment.isFromFeaturedRootFragment) {
+ forMedia(
+ mediaDetailPagerFragment.position + i,
+ mediaDetailPagerFragment.editable, mediaDetailPagerFragment.isFeaturedImage,
+ mediaDetailPagerFragment.isWikipediaButtonDisplayed
+ )
+ } else {
+ forMedia(
+ i, mediaDetailPagerFragment.editable,
+ mediaDetailPagerFragment.isFeaturedImage,
+ mediaDetailPagerFragment.isWikipediaButtonDisplayed
+ )
+ }
+ }
+
+ override fun getCount(): Int {
+ if (mediaDetailPagerFragment.activity == null) {
+ Timber.d("Skipping getCount. Returning as activity is destroyed!")
+ return 0
+ }
+ return mediaDetailPagerFragment.mediaDetailProvider!!.getTotalMediaCount()
+ }
+
+ /**
+ * If current fragment is of type MediaDetailFragment, return it, otherwise return null.
+ *
+ * @return MediaDetailFragment
+ */
+ val currentMediaDetailFragment: MediaDetailFragment?
+ get() = currentFragment as? MediaDetailFragment
+
+ /**
+ * Called to inform the adapter of which item is currently considered to be the "primary", that
+ * is the one show to the user as the current page.
+ */
+ override fun setPrimaryItem(
+ container: ViewGroup, position: Int,
+ obj: Any
+ ) {
+ // Update the current fragment if changed
+ if (currentFragment !== obj) {
+ currentFragment = (obj as Fragment)
+ }
+ super.setPrimaryItem(container, position, obj)
+ }
+}
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 77ff1df0c..41e65ae4e 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,7 +16,6 @@ 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
@@ -75,11 +74,9 @@ import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CameraPosition
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.Companion.instance
-import fr.free.nrw.commons.locationpicker.LocationPicker
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.actions.ThanksClient
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
@@ -103,7 +100,7 @@ import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.language.AppLanguageLookUpTable
import fr.free.nrw.commons.location.LocationServiceManager
-import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
+import fr.free.nrw.commons.locationpicker.LocationPicker
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewHelper
import fr.free.nrw.commons.settings.Prefs
@@ -117,8 +114,13 @@ import fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources
import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE
import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction
import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
+import fr.free.nrw.commons.utils.ViewUtil
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
import fr.free.nrw.commons.utils.ViewUtilWrapper
+import fr.free.nrw.commons.utils.copyToClipboard
+import fr.free.nrw.commons.utils.handleGeoCoordinates
+import fr.free.nrw.commons.utils.handleWebUrl
+import fr.free.nrw.commons.utils.setUnderlinedText
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage.Revision
import io.reactivex.Observable
import io.reactivex.Single
@@ -126,6 +128,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.apache.commons.lang3.StringUtils
import timber.log.Timber
+import java.lang.String.format
import java.util.Date
import java.util.Locale
import java.util.Objects
@@ -315,14 +318,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
_binding = FragmentMediaDetailBinding.inflate(inflater, container, false)
val view: View = binding.root
-
- Utils.setUnderlinedText(binding.seeMore, R.string.nominated_see_more, requireContext())
-
- if (isCategoryImage) {
- binding.authorLinearLayout.visibility = View.VISIBLE
- } else {
- binding.authorLinearLayout.visibility = View.GONE
- }
+ binding.seeMore.setUnderlinedText(R.string.nominated_see_more)
if (!sessionManager.isUserLoggedIn) {
binding.categoryEditButton.visibility = View.GONE
@@ -545,6 +541,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
)
binding.progressBarEdit.visibility = View.GONE
+ binding.descriptionEdit.visibility = View.VISIBLE
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -622,10 +619,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
- { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) },
+ { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) },
{ t: Throwable? -> Timber.e(t) })
)
- // compositeDisposable.add(disposable);
}
private fun onDiscussionLoaded(discussion: String) {
@@ -655,7 +651,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
}
- private fun onDepictionsLoaded(idAndCaptions: List) {
+ private fun onDepictionsLoaded(idAndCaptions: List) {
binding.depictsLayout.visibility = View.VISIBLE
binding.depictionsEditButton.visibility = View.VISIBLE
buildDepictionList(idAndCaptions)
@@ -813,10 +809,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
categoryNames.clear()
categoryNames.addAll(media.categories!!)
- if (media.author == null || media.author == "") {
- binding.authorLinearLayout.visibility = View.GONE
- } else {
- binding.mediaDetailAuthor.text = media.author
+ // Show author or uploader information for licensing compliance
+ val authorName = media.getAttributedAuthor()
+ val uploaderName = media.user
+
+ when {
+ !authorName.isNullOrEmpty() -> {
+ // Show author if available
+ binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_author)
+ binding.mediaDetailAuthor.text = authorName
+ binding.authorLinearLayout.visibility = View.VISIBLE
+ }
+ !uploaderName.isNullOrEmpty() -> {
+ // Show uploader as fallback
+ binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_uploader)
+ binding.mediaDetailAuthor.text = uploaderName
+ binding.authorLinearLayout.visibility = View.VISIBLE
+ }
+ else -> {
+ // Hide if neither author nor uploader is available
+ binding.authorLinearLayout.visibility = View.GONE
+ }
}
}
@@ -865,24 +878,24 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
* Populates media details fragment with depiction list
* @param idAndCaptions
*/
- private fun buildDepictionList(idAndCaptions: List) {
+ private fun buildDepictionList(idAndCaptions: List) {
binding.mediaDetailDepictionContainer.removeAllViews()
// Create a mutable list from the original list
val mutableIdAndCaptions = idAndCaptions.toMutableList()
if (mutableIdAndCaptions.isEmpty()) {
- // Create a placeholder IdAndCaptions object and add it to the list
+ // Create a placeholder IdAndLabels object and add it to the list
mutableIdAndCaptions.add(
- IdAndCaptions(
+ IdAndLabels(
id = media?.pageId ?: "", // Use an empty string if media?.pageId is null
- captions = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
+ labels = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
)
)
}
val locale: String = Locale.getDefault().language
- for (idAndCaption: IdAndCaptions in mutableIdAndCaptions) {
+ for (idAndCaption in mutableIdAndCaptions) {
binding.mediaDetailDepictionContainer.addView(
buildDepictLabel(
getDepictionCaption(idAndCaption, locale),
@@ -894,22 +907,22 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
- private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? {
+ private fun getDepictionCaption(idAndCaption: IdAndLabels, locale: String): String? {
// Check if the Depiction Caption is available in user's locale
// if not then check for english, else show any available.
- if (idAndCaption.captions[locale] != null) {
- return idAndCaption.captions[locale]
+ if (idAndCaption.labels[locale] != null) {
+ return idAndCaption.labels[locale]
}
- if (idAndCaption.captions["en"] != null) {
- return idAndCaption.captions["en"]
+ if (idAndCaption.labels["en"] != null) {
+ return idAndCaption.labels["en"]
}
- return idAndCaption.captions.values.iterator().next()
+ return idAndCaption.labels.values.iterator().next()
}
private fun onMediaDetailLicenceClicked() {
val url: String? = media!!.licenseUrl
if (!StringUtils.isBlank(url) && activity != null) {
- Utils.handleWebUrl(activity, Uri.parse(url))
+ handleWebUrl(requireContext(), Uri.parse(url))
} else {
viewUtil.showShortToast(requireActivity(), getString(R.string.null_url))
}
@@ -917,17 +930,17 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
private fun onMediaDetailCoordinatesClicked() {
if (media!!.coordinates != null && activity != null) {
- Utils.handleGeoCoordinates(activity, media!!.coordinates)
+ handleGeoCoordinates(requireContext(), media!!.coordinates!!)
}
}
private fun onCopyWikicodeClicked() {
val data: String =
"[[" + media!!.filename + "|thumb|" + media!!.fallbackDescription + "]]"
- Utils.copy("wikiCode", data, context)
+ requireContext().copyToClipboard("wikiCode", data)
Timber.d("Generated wikidata copy code: %s", data)
- Toast.makeText(context, getString(R.string.wikicode_copied), Toast.LENGTH_SHORT)
+ Toast.makeText(requireContext(), getString(R.string.wikicode_copied), Toast.LENGTH_SHORT)
.show()
}
@@ -1014,12 +1027,12 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
val message: String = if (result) {
context.getString(
R.string.send_thank_success_message,
- media!!.displayTitle
+ media!!.user
)
} else {
context.getString(
R.string.send_thank_failure_message,
- media!!.displayTitle
+ media!!.user
)
}
@@ -1648,7 +1661,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
getString(R.string.cancel),
{
val reason: String = input.text.toString()
- onDeleteClickeddialogtext(reason)
+ onDeleteClickedDialogText(reason)
},
{},
input
@@ -1702,26 +1715,48 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
resultSingle
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe { _ ->
- if (applicationKvStore.getBoolean(
- String.format(
- NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl
- ), false
- )
- ) {
- applicationKvStore.remove(
- String.format(
- NOMINATING_FOR_DELETION_MEDIA,
- media!!.imageUrl
- )
- )
- callback!!.nominatingForDeletion(index)
- }
- }
+ .subscribe(this::handleDeletionResult, this::handleDeletionError);
+ }
+
+ /**
+ * Disables Progress Bar and Update delete button text.
+ */
+ private fun disableProgressBar() {
+ activity?.run {
+ runOnUiThread(Runnable {
+ binding.progressBarDeletion.visibility = View.GONE
+ })
+ } ?: return // Prevent NullPointerException when fragment is not attached to activity
+ }
+
+ private fun handleDeletionResult(success: Boolean) {
+ if (success) {
+ binding.nominateDeletion.text = getString(R.string.nominated_for_deletion_btn)
+ ViewUtil.showLongSnackbar(requireView(), getString(R.string.nominated_for_deletion))
+ disableProgressBar()
+ checkAndClearDeletionFlag()
+ } else {
+ disableProgressBar()
+ }
+ }
+
+ private fun handleDeletionError(throwable: Throwable) {
+ throwable.printStackTrace()
+ disableProgressBar()
+ checkAndClearDeletionFlag()
+ }
+
+ private fun checkAndClearDeletionFlag() {
+ if (applicationKvStore
+ .getBoolean(format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl), false)
+ ) {
+ applicationKvStore.remove(format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl))
+ callback!!.nominatingForDeletion(index)
+ }
}
@SuppressLint("CheckResult")
- private fun onDeleteClickeddialogtext(reason: String) {
+ private fun onDeleteClickedDialogText(reason: String) {
applicationKvStore.putBoolean(
String.format(
NOMINATING_FOR_DELETION_MEDIA,
@@ -1738,27 +1773,12 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
resultSingletext
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe { _ ->
- if (applicationKvStore.getBoolean(
- String.format(
- NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl
- ), false
- )
- ) {
- applicationKvStore.remove(
- String.format(
- NOMINATING_FOR_DELETION_MEDIA,
- media!!.imageUrl
- )
- )
- callback!!.nominatingForDeletion(index)
- }
- }
+ .subscribe(this::handleDeletionResult, this::handleDeletionError);
}
private fun onSeeMoreClicked() {
if (binding.nominatedDeletionBanner.visibility == View.VISIBLE && activity != null) {
- Utils.handleWebUrl(activity, Uri.parse(media!!.pageTitle.mobileUri))
+ handleWebUrl(requireContext(), Uri.parse(media!!.pageTitle.mobileUri))
}
}
@@ -2109,22 +2129,17 @@ fun FileUsagesContainer(
val uriHandle = LocalUriHandler.current
Column(modifier = modifier) {
-
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
-
Text(
text = stringResource(R.string.usages_on_commons_heading),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall
)
-
- IconButton(onClick = {
- isCommonsListExpanded = !isCommonsListExpanded
- }) {
+ IconButton(onClick = { isCommonsListExpanded = !isCommonsListExpanded }) {
Icon(
imageVector = if (isCommonsListExpanded) Icons.Default.KeyboardArrowUp
else Icons.Default.KeyboardArrowDown,
@@ -2138,11 +2153,8 @@ fun FileUsagesContainer(
MediaDetailViewModel.FileUsagesContainerState.Loading -> {
LinearProgressIndicator()
}
-
is MediaDetailViewModel.FileUsagesContainerState.Success -> {
-
val data = commonsContainerState.data
-
if (data.isNullOrEmpty()) {
ListItem(headlineContent = {
Text(
@@ -2162,7 +2174,7 @@ fun FileUsagesContainer(
headlineContent = {
Text(
modifier = Modifier.clickable {
- uriHandle.openUri(usage.link!!)
+ usage.link?.let { uriHandle.openUri(it) }
},
text = usage.title,
style = MaterialTheme.typography.titleSmall.copy(
@@ -2170,11 +2182,11 @@ fun FileUsagesContainer(
textDecoration = TextDecoration.Underline
)
)
- })
+ }
+ )
}
}
}
-
is MediaDetailViewModel.FileUsagesContainerState.Error -> {
ListItem(headlineContent = {
Text(
@@ -2184,12 +2196,10 @@ fun FileUsagesContainer(
)
})
}
-
MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
}
}
-
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -2200,10 +2210,7 @@ fun FileUsagesContainer(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall
)
-
- IconButton(onClick = {
- isOtherWikisListExpanded = !isOtherWikisListExpanded
- }) {
+ IconButton(onClick = { isOtherWikisListExpanded = !isOtherWikisListExpanded }) {
Icon(
imageVector = if (isOtherWikisListExpanded) Icons.Default.KeyboardArrowUp
else Icons.Default.KeyboardArrowDown,
@@ -2217,11 +2224,8 @@ fun FileUsagesContainer(
MediaDetailViewModel.FileUsagesContainerState.Loading -> {
LinearProgressIndicator()
}
-
is MediaDetailViewModel.FileUsagesContainerState.Success -> {
-
val data = globalContainerState.data
-
if (data.isNullOrEmpty()) {
ListItem(headlineContent = {
Text(
@@ -2240,16 +2244,20 @@ fun FileUsagesContainer(
},
headlineContent = {
Text(
+ modifier = Modifier.clickable {
+ usage.link?.let { uriHandle.openUri(it) }
+ },
text = usage.title,
style = MaterialTheme.typography.titleSmall.copy(
+ color = Color(0xFF5A6AEC),
textDecoration = TextDecoration.Underline
)
)
- })
+ }
+ )
}
}
}
-
is MediaDetailViewModel.FileUsagesContainerState.Error -> {
ListItem(headlineContent = {
Text(
@@ -2259,10 +2267,8 @@ fun FileUsagesContainer(
)
})
}
-
MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
}
}
-
}
-}
+}
\ No newline at end of file
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
deleted file mode 100644
index cba582a35..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
+++ /dev/null
@@ -1,678 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import static fr.free.nrw.commons.Utils.handleWebUrl;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.widget.ProgressBar;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toast;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentStatePagerAdapter;
-import androidx.viewpager.widget.ViewPager;
-import com.google.android.material.snackbar.Snackbar;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.bookmarks.models.Bookmark;
-import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
-import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.contributions.MainActivity;
-import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
-import fr.free.nrw.commons.profile.ProfileActivity;
-import fr.free.nrw.commons.utils.DownloadUtils;
-import fr.free.nrw.commons.utils.ImageUtils;
-import fr.free.nrw.commons.utils.NetworkUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.schedulers.Schedulers;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Objects;
-import java.util.concurrent.Callable;
-import javax.inject.Inject;
-import timber.log.Timber;
-
-public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener, MediaDetailFragment.Callback {
-
- @Inject BookmarkPicturesDao bookmarkDao;
-
- @Inject
- protected OkHttpJsonApiClient okHttpJsonApiClient;
-
- @Inject
- protected SessionManager sessionManager;
-
- private static CompositeDisposable compositeDisposable = new CompositeDisposable();
-
- private FragmentMediaDetailPagerBinding binding;
-
- private boolean editable;
- private boolean isFeaturedImage;
- private boolean isWikipediaButtonDisplayed;
- MediaDetailAdapter adapter;
- private Bookmark bookmark;
- private MediaDetailProvider provider;
- private boolean isFromFeaturedRootFragment;
- private int position;
-
- /**
- * ProgressBar used to indicate the loading status of media items.
- */
- private ProgressBar imageProgressBar;
-
- private ArrayList removedItems=new ArrayList();
-
- public void clearRemoved(){
- removedItems.clear();
- }
- public ArrayList getRemovedItems() {
- return removedItems;
- }
-
-
- /**
- * Use this factory method to create a new instance of this fragment using the provided
- * parameters.
- *
- * This method will create a new instance of MediaDetailPagerFragment and the arguments will be
- * saved to a bundle which will be later available in the {@link #onCreate(Bundle)}
- * @param editable
- * @param isFeaturedImage
- * @return
- */
- public static MediaDetailPagerFragment newInstance(boolean editable, boolean isFeaturedImage) {
- MediaDetailPagerFragment mediaDetailPagerFragment = new MediaDetailPagerFragment();
- Bundle args = new Bundle();
- args.putBoolean("is_editable", editable);
- args.putBoolean("is_featured_image", isFeaturedImage);
- mediaDetailPagerFragment.setArguments(args);
- return mediaDetailPagerFragment;
- }
-
- public MediaDetailPagerFragment() {
- // Required empty public constructor
- };
-
-
- @Override
- public View onCreateView(LayoutInflater inflater,
- ViewGroup container,
- Bundle savedInstanceState) {
- binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false);
- binding.mediaDetailsPager.addOnPageChangeListener(this);
- // Initialize the ProgressBar by finding it in the layout
- imageProgressBar = binding.getRoot().findViewById(R.id.itemProgressBar);
- adapter = new MediaDetailAdapter(getChildFragmentManager());
-
- // ActionBar is now supported in both activities - if this crashes something is quite wrong
- final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- else {
- throw new AssertionError("Action bar should not be null!");
- }
-
- // If fragment is associated with ProfileActivity, then hide the tabLayout
- if (getActivity() instanceof ProfileActivity) {
- ((ProfileActivity)getActivity()).setTabLayoutVisibility(false);
- }
-
- // Else if fragment is associated with MainActivity then hide that tab layout
- else if (getActivity() instanceof MainActivity) {
- ((MainActivity)getActivity()).hideTabs();
- }
-
- binding.mediaDetailsPager.setAdapter(adapter);
-
- if (savedInstanceState != null) {
- final int pageNumber = savedInstanceState.getInt("current-page");
- binding.mediaDetailsPager.setCurrentItem(pageNumber, false);
- getActivity().invalidateOptionsMenu();
- }
- adapter.notifyDataSetChanged();
-
- return binding.getRoot();
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem());
- outState.putBoolean("editable", editable);
- outState.putBoolean("isFeaturedImage", isFeaturedImage);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- editable = savedInstanceState.getBoolean("editable", false);
- isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false);
-
- }
- setHasOptionsMenu(true);
- initProvider();
- }
-
- /**
- * initialise the provider, based on from where the fragment was started, as in from an activity
- * or a fragment
- */
- private void initProvider() {
- if (getParentFragment() instanceof MediaDetailProvider) {
- provider = (MediaDetailProvider) getParentFragment();
- } else if (getActivity() instanceof MediaDetailProvider) {
- provider = (MediaDetailProvider) getActivity();
- } else {
- throw new ClassCastException("Parent must implement MediaDetailProvider");
- }
- }
-
- public MediaDetailProvider getMediaDetailProvider() {
- return provider;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (getActivity() == null) {
- Timber.d("Returning as activity is destroyed!");
- return true;
- }
-
- Media m = provider.getMediaAtPosition(binding.mediaDetailsPager.getCurrentItem());
- MediaDetailFragment mediaDetailFragment = this.adapter.getCurrentMediaDetailFragment();
- switch (item.getItemId()) {
- case R.id.menu_bookmark_current_image:
- boolean bookmarkExists = bookmarkDao.updateBookmark(bookmark);
- Snackbar snackbar = bookmarkExists ? Snackbar.make(getView(), R.string.add_bookmark, Snackbar.LENGTH_LONG) : Snackbar.make(getView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG);
- snackbar.show();
- updateBookmarkState(item);
- return true;
- case R.id.menu_copy_link:
- String uri = m.getPageTitle().getCanonicalUri();
- Utils.copy("shareLink", uri, requireContext());
- Timber.d("Copied share link to clipboard: %s", uri);
- Toast.makeText(requireContext(), getString(R.string.menu_link_copied),
- Toast.LENGTH_SHORT).show();
- return true;
- case R.id.menu_share_current_image:
- Intent shareIntent = new Intent(Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getPageTitle().getCanonicalUri());
- startActivity(Intent.createChooser(shareIntent, "Share image via..."));
-
- //Add media detail to backstack when the share button is clicked
- //So that when the share is cancelled or completed the media detail page is on top
- // of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
- FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
- if (supportFragmentManager.getBackStackEntryCount() < 2) {
- supportFragmentManager
- .beginTransaction()
- .addToBackStack(MediaDetailPagerFragment.class.getName())
- .commit();
- supportFragmentManager.executePendingTransactions();
- }
- return true;
- case R.id.menu_browser_current_image:
- // View in browser
- handleWebUrl(requireContext(), Uri.parse(m.getPageTitle().getMobileUri()));
- return true;
- case R.id.menu_download_current_image:
- // Download
- if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
- ViewUtil.showShortSnackbar(getView(), R.string.no_internet);
- return false;
- }
- DownloadUtils.downloadMedia(getActivity(), m);
- return true;
- case R.id.menu_set_as_wallpaper:
- // Set wallpaper
- setWallpaper(m);
- return true;
- case R.id.menu_set_as_avatar:
- // Set avatar
- setAvatar(m);
- return true;
- case R.id.menu_view_user_page:
- if (m != null && m.getUser() != null) {
- ProfileActivity.startYourself(getActivity(), m.getUser(),
- !Objects.equals(sessionManager.getUserName(), m.getUser()));
- }
- return true;
- case R.id.menu_view_report:
- showReportDialog(m);
- case R.id.menu_view_set_white_background:
- if (mediaDetailFragment != null) {
- mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.white));
- }
- return true;
- case R.id.menu_view_set_black_background:
- if (mediaDetailFragment != null) {
- mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.black));
- }
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- private void showReportDialog(final Media media) {
- if (media == null) {
- return;
- }
- final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
- final String[] values = requireContext().getResources()
- .getStringArray(R.array.report_violation_options);
- builder.setTitle(R.string.report_violation);
- builder.setItems(R.array.report_violation_options, (dialog, which) -> {
- sendReportEmail(media, values[which]);
- });
- builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
- builder.setCancelable(false);
- builder.show();
- }
-
- private void sendReportEmail(final Media media, final String type) {
- final String technicalInfo = getTechInfo(media, type);
-
- final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
- feedbackIntent.setType("message/rfc822");
- feedbackIntent.setData(Uri.parse("mailto:"));
- feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
- new String[]{CommonsApplication.REPORT_EMAIL});
- feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
- CommonsApplication.REPORT_EMAIL_SUBJECT);
- feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo);
- try {
- startActivity(feedbackIntent);
- } catch (final ActivityNotFoundException e) {
- Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
- }
- }
-
- private String getTechInfo(final Media media, final String type) {
- final StringBuilder builder = new StringBuilder();
-
- builder.append("Report type: ")
- .append(type)
- .append("\n\n");
-
- builder.append("Image that you want to report: ")
- .append(media.getImageUrl())
- .append("\n\n");
-
- builder.append("User that you want to report: ")
- .append(media.getUser())
- .append("\n\n");
-
- if (sessionManager.getUserName() != null) {
- builder.append("Your username: ")
- .append(sessionManager.getUserName())
- .append("\n\n");
- }
-
- builder.append("Violation reason: ")
- .append("\n");
-
- builder.append("----------------------------------------------")
- .append("\n")
- .append("(please write reason here)")
- .append("\n")
- .append("----------------------------------------------")
- .append("\n\n")
- .append("Thank you for your report! Our team will investigate as soon as possible.")
- .append("\n")
- .append("Please note that images also have a `Nominate for deletion` button.");
-
- return builder.toString();
- }
-
- /**
- * Set the media as the device's wallpaper if the imageUrl is not null
- * Fails silently if setting the wallpaper fails
- * @param media
- */
- private void setWallpaper(Media media) {
- if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
- Timber.d("Media URL not present");
- return;
- }
- ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
- }
-
- /**
- * Set the media as user's leaderboard avatar
- * @param media
- */
- private void setAvatar(Media media) {
- if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
- Timber.d("Media URL not present");
- return;
- }
- ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(),
- Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
- okHttpJsonApiClient, compositeDisposable);
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- if (!editable) { // Disable menu options for editable views
- menu.clear(); // see http://stackoverflow.com/a/8495697/17865
- inflater.inflate(R.menu.fragment_image_detail, menu);
- if (binding.mediaDetailsPager != null) {
- MediaDetailProvider provider = getMediaDetailProvider();
- if(provider == null) {
- return;
- }
- final int position;
- if (isFromFeaturedRootFragment) {
- position = this.position;
- } else {
- position = binding.mediaDetailsPager.getCurrentItem();
- }
-
- Media m = provider.getMediaAtPosition(position);
- if (m != null) {
- // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
- menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
- menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true);
- menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
- menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
- menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
- menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true);
- if (m.getUser() != null) {
- menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true);
- }
-
- try {
- URL mediaUrl = new URL(m.getImageUrl());
- this.handleBackgroundColorMenuItems(
- () -> BitmapFactory.decodeStream(mediaUrl.openConnection().getInputStream()),
- menu
- );
- } catch (Exception e) {
- Timber.e("Cant detect media transparency");
- }
-
- // Initialize bookmark object
- bookmark = new Bookmark(
- m.getFilename(),
- m.getAuthorOrUser(),
- BookmarkPicturesContentProvider.uriForName(m.getFilename())
- );
- updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image));
- final Integer contributionState = provider.getContributionStateAt(position);
- if (contributionState != null) {
- switch (contributionState) {
- case Contribution.STATE_FAILED:
- case Contribution.STATE_IN_PROGRESS:
- case Contribution.STATE_QUEUED:
- menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_copy_link).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_share_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_download_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
- .setVisible(false);
- break;
- case Contribution.STATE_COMPLETED:
- // Default set of menu items works fine. Treat same as regular media object
- break;
- }
- }
- } else {
- menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_copy_link).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_share_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_download_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
- .setVisible(false);
- menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
- .setVisible(false);
- }
-
- if (!sessionManager.isUserLoggedIn()) {
- menu.findItem(R.id.menu_set_as_avatar).setVisible(false);
- }
-
- }
- }
- }
-
- /**
- * Decide wether or not we should display the background color menu items
- * We display them if the image is transparent
- * @param getBitmap
- * @param menu
- */
- private void handleBackgroundColorMenuItems(Callable getBitmap, Menu menu) {
- Observable.fromCallable(
- getBitmap
- ).subscribeOn(Schedulers.newThread())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(image -> {
- if (image.hasAlpha()) {
- menu.findItem(R.id.menu_view_set_white_background).setVisible(true).setEnabled(true);
- menu.findItem(R.id.menu_view_set_black_background).setVisible(true).setEnabled(true);
- }
- });
- }
-
- private void updateBookmarkState(MenuItem item) {
- boolean isBookmarked = bookmarkDao.findBookmark(bookmark);
- if(isBookmarked) {
- if(removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
- removedItems.remove(new Integer(binding.mediaDetailsPager.getCurrentItem()));
- }
- }
- else {
- if(!removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
- removedItems.add(binding.mediaDetailsPager.getCurrentItem());
- }
- }
- int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px;
- item.setIcon(icon);
- }
-
- public void showImage(int i, boolean isWikipediaButtonDisplayed) {
- this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed;
- setViewPagerCurrentItem(i);
- }
-
- public void showImage(int i) {
- setViewPagerCurrentItem(i);
- }
-
- /**
- * This function waits for the item to load then sets the item to current item
- * @param position current item that to be shown
- */
- private void setViewPagerCurrentItem(int position) {
-
- final Handler handler = new Handler(Looper.getMainLooper());
- final Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // Show the ProgressBar while waiting for the item to load
- imageProgressBar.setVisibility(View.VISIBLE);
- // Check if the adapter has enough items loaded
- if(adapter.getCount() > position){
- // Set the current item in the ViewPager
- binding.mediaDetailsPager.setCurrentItem(position, false);
- // Hide the ProgressBar once the item is loaded
- imageProgressBar.setVisibility(View.GONE);
- } else {
- // If the item is not ready yet, post the Runnable again
- handler.post(this);
- }
- }
- };
- // Start the Runnable
- handler.post(runnable);
- }
-
- /**
- * The method notify the viewpager that number of items have changed.
- */
- public void notifyDataSetChanged(){
- if (null != adapter) {
- adapter.notifyDataSetChanged();
- }
- }
-
- @Override
- public void onPageScrolled(int i, float v, int i2) {
- if(getActivity() == null) {
- Timber.d("Returning as activity is destroyed!");
- return;
- }
-
- getActivity().invalidateOptionsMenu();
- }
-
- @Override
- public void onPageSelected(int i) {
- }
-
- @Override
- public void onPageScrollStateChanged(int i) {
- }
-
- public void onDataSetChanged() {
- if (null != adapter) {
- adapter.notifyDataSetChanged();
- }
- }
-
- /**
- * Called after the media is nominated for deletion
- *
- * @param index item position that has been nominated
- */
- @Override
- public void nominatingForDeletion(int index) {
- provider.refreshNominatedMedia(index);
- }
-
- public interface MediaDetailProvider {
- Media getMediaAtPosition(int i);
-
- int getTotalMediaCount();
-
- Integer getContributionStateAt(int position);
-
- // Reload media detail fragment once media is nominated
- void refreshNominatedMedia(int index);
- }
-
- //FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
- private class MediaDetailAdapter extends FragmentStatePagerAdapter {
-
- /**
- * Keeps track of the current displayed fragment.
- */
- private Fragment mCurrentFragment;
-
- public MediaDetailAdapter(FragmentManager fm) {
- super(fm);
- }
-
- @Override
- public Fragment getItem(int i) {
- if (i == 0) {
- // See bug https://code.google.com/p/android/issues/detail?id=27526
- if(getActivity() == null) {
- Timber.d("Skipping getItem. Returning as activity is destroyed!");
- return null;
- }
- binding.mediaDetailsPager.postDelayed(() -> getActivity().invalidateOptionsMenu(), 5);
- }
- if (isFromFeaturedRootFragment) {
- return MediaDetailFragment.forMedia(position+i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
- } else {
- return MediaDetailFragment.forMedia(i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
- }
- }
-
- @Override
- public int getCount() {
- if (getActivity() == null) {
- Timber.d("Skipping getCount. Returning as activity is destroyed!");
- return 0;
- }
- return provider.getTotalMediaCount();
- }
-
- /**
- * Get the currently displayed fragment.
- * @return
- */
- public Fragment getCurrentFragment() {
- return mCurrentFragment;
- }
-
- /**
- * If current fragment is of type MediaDetailFragment, return it, otherwise return null.
- * @return MediaDetailFragment
- */
- public MediaDetailFragment getCurrentMediaDetailFragment() {
- if (mCurrentFragment instanceof MediaDetailFragment) {
- return (MediaDetailFragment) mCurrentFragment;
- }
-
- return null;
- }
-
- /**
- * Called to inform the adapter of which item is currently considered to be the "primary",
- * that is the one show to the user as the current page.
- * @param container
- * @param position
- * @param object
- */
- @Override
- public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
- @NonNull final Object object) {
- // Update the current fragment if changed
- if(getCurrentFragment() != object) {
- mCurrentFragment = ((Fragment)object);
- }
- super.setPrimaryItem(container, position, object);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt
new file mode 100644
index 000000000..92cca611e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt
@@ -0,0 +1,622 @@
+package fr.free.nrw.commons.media
+
+import android.content.ActivityNotFoundException
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ProgressBar
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import com.google.android.material.snackbar.Snackbar
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.auth.SessionManager
+import fr.free.nrw.commons.bookmarks.models.Bookmark
+import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
+import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
+import fr.free.nrw.commons.contributions.Contribution
+import fr.free.nrw.commons.contributions.MainActivity
+import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
+import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
+import fr.free.nrw.commons.profile.ProfileActivity
+import fr.free.nrw.commons.profile.ProfileActivity.Companion.startYourself
+import fr.free.nrw.commons.utils.ClipboardUtils.copy
+import fr.free.nrw.commons.utils.DownloadUtils.downloadMedia
+import fr.free.nrw.commons.utils.ImageUtils.setAvatarFromImageUrl
+import fr.free.nrw.commons.utils.ImageUtils.setWallpaperFromImageUrl
+import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
+import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
+import fr.free.nrw.commons.utils.handleWebUrl
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.functions.Consumer
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
+import java.net.URL
+import java.util.concurrent.Callable
+import javax.inject.Inject
+import androidx.core.net.toUri
+
+class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeListener,
+ MediaDetailFragment.Callback {
+ @JvmField
+ @Inject
+ var bookmarkDao: BookmarkPicturesDao? = null
+
+ @JvmField
+ @Inject
+ var okHttpJsonApiClient: OkHttpJsonApiClient? = null
+
+ @JvmField
+ @Inject
+ var sessionManager: SessionManager? = null
+
+ var binding: FragmentMediaDetailPagerBinding? = null
+ var editable: Boolean = false
+ var isFeaturedImage: Boolean = false
+ var isWikipediaButtonDisplayed: Boolean = false
+ var adapter: MediaDetailAdapter? = null
+ var bookmark: Bookmark? = null
+ var mediaDetailProvider: MediaDetailProvider? = null
+ var isFromFeaturedRootFragment: Boolean = false
+ var position: Int = 0
+
+ /**
+ * ProgressBar used to indicate the loading status of media items.
+ */
+ var imageProgressBar: ProgressBar? = null
+
+ var removedItems: ArrayList = ArrayList()
+
+ fun clearRemoved() = removedItems.clear()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false)
+ binding!!.mediaDetailsPager.addOnPageChangeListener(this)
+ // Initialize the ProgressBar by finding it in the layout
+ imageProgressBar = binding!!.root.findViewById(R.id.itemProgressBar)
+ adapter = MediaDetailAdapter(this, childFragmentManager)
+
+ // ActionBar is now supported in both activities - if this crashes something is quite wrong
+ val actionBar = (activity as AppCompatActivity).supportActionBar
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true)
+ } else {
+ throw AssertionError("Action bar should not be null!")
+ }
+
+ // If fragment is associated with ProfileActivity, then hide the tabLayout
+ if (activity is ProfileActivity) {
+ (activity as ProfileActivity).setTabLayoutVisibility(false)
+ } else if (activity is MainActivity) {
+ (activity as MainActivity).hideTabs()
+ }
+
+ binding!!.mediaDetailsPager.adapter = adapter
+
+ if (savedInstanceState != null) {
+ val pageNumber = savedInstanceState.getInt("current-page")
+ binding!!.mediaDetailsPager.setCurrentItem(pageNumber, false)
+ requireActivity().invalidateOptionsMenu()
+ }
+ adapter!!.notifyDataSetChanged()
+
+ return binding!!.root
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt("current-page", binding!!.mediaDetailsPager.currentItem)
+ outState.putBoolean("editable", editable)
+ outState.putBoolean("isFeaturedImage", isFeaturedImage)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ editable = savedInstanceState.getBoolean("editable", false)
+ isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false)
+ }
+ setHasOptionsMenu(true)
+ initProvider()
+ }
+
+ /**
+ * initialise the provider, based on from where the fragment was started, as in from an activity
+ * or a fragment
+ */
+ private fun initProvider() {
+ if (parentFragment is MediaDetailProvider) {
+ mediaDetailProvider = parentFragment as MediaDetailProvider
+ } else if (activity is MediaDetailProvider) {
+ mediaDetailProvider = activity as MediaDetailProvider?
+ } else {
+ throw ClassCastException("Parent must implement MediaDetailProvider")
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (activity == null) {
+ Timber.d("Returning as activity is destroyed!")
+ return true
+ }
+
+ val m = mediaDetailProvider!!.getMediaAtPosition(binding!!.mediaDetailsPager.currentItem)
+ val mediaDetailFragment = adapter!!.currentMediaDetailFragment
+ when (item.itemId) {
+ R.id.menu_bookmark_current_image -> {
+ val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!)
+ val snackbar = if (bookmarkExists) Snackbar.make(
+ requireView(),
+ R.string.add_bookmark,
+ Snackbar.LENGTH_LONG
+ ) else Snackbar.make(
+ requireView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG
+ )
+ snackbar.show()
+ updateBookmarkState(item)
+ return true
+ }
+
+ R.id.menu_copy_link -> {
+ val uri = m!!.pageTitle.canonicalUri
+ copy("shareLink", uri, requireContext())
+ Timber.d("Copied share link to clipboard: %s", uri)
+ Toast.makeText(
+ requireContext(), getString(R.string.menu_link_copied),
+ Toast.LENGTH_SHORT
+ ).show()
+ return true
+ }
+
+ R.id.menu_share_current_image -> {
+ val shareIntent = Intent(Intent.ACTION_SEND)
+ shareIntent.setType("text/plain")
+ shareIntent.putExtra(
+ Intent.EXTRA_TEXT, """${m!!.displayTitle}
+${m.pageTitle.canonicalUri}"""
+ )
+ startActivity(Intent.createChooser(shareIntent, "Share image via..."))
+
+ //Add media detail to backstack when the share button is clicked
+ //So that when the share is cancelled or completed the media detail page is on top
+ // of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
+ val supportFragmentManager = requireActivity().supportFragmentManager
+ if (supportFragmentManager.backStackEntryCount < 2) {
+ supportFragmentManager
+ .beginTransaction()
+ .addToBackStack(MediaDetailPagerFragment::class.java.name)
+ .commit()
+ supportFragmentManager.executePendingTransactions()
+ }
+ return true
+ }
+
+ R.id.menu_browser_current_image -> {
+ // View in browser
+ handleWebUrl(requireContext(), m!!.pageTitle.mobileUri.toUri())
+ return true
+ }
+
+ R.id.menu_download_current_image -> {
+ // Download
+ if (!isInternetConnectionEstablished(activity)) {
+ showShortSnackbar(requireView(), R.string.no_internet)
+ return false
+ }
+ downloadMedia(activity, m!!)
+ return true
+ }
+
+ R.id.menu_set_as_wallpaper -> {
+ // Set wallpaper
+ setWallpaper(m!!)
+ return true
+ }
+
+ R.id.menu_set_as_avatar -> {
+ // Set avatar
+ setAvatar(m!!)
+ return true
+ }
+
+ R.id.menu_view_user_page -> {
+ if (m?.user != null) {
+ startYourself(
+ requireActivity(), m.user!!,
+ sessionManager!!.userName != m.user
+ )
+ }
+ return true
+ }
+
+ R.id.menu_view_report -> {
+ showReportDialog(m)
+ mediaDetailFragment?.onImageBackgroundChanged(
+ ContextCompat.getColor(
+ requireContext(),
+ R.color.white
+ )
+ )
+ return true
+ }
+
+ R.id.menu_view_set_white_background -> {
+ mediaDetailFragment?.onImageBackgroundChanged(
+ ContextCompat.getColor(
+ requireContext(),
+ R.color.white
+ )
+ )
+ return true
+ }
+
+ R.id.menu_view_set_black_background -> {
+ mediaDetailFragment?.onImageBackgroundChanged(
+ ContextCompat.getColor(
+ requireContext(),
+ R.color.black
+ )
+ )
+ return true
+ }
+
+ else -> return super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun showReportDialog(media: Media?) {
+ if (media == null) {
+ return
+ }
+ val builder = AlertDialog.Builder(requireActivity())
+ val values = requireContext().resources
+ .getStringArray(R.array.report_violation_options)
+ builder.setTitle(R.string.report_violation)
+ builder.setItems(
+ R.array.report_violation_options
+ ) { dialog: DialogInterface?, which: Int ->
+ sendReportEmail(media, values[which])
+ }
+ builder.setNegativeButton(
+ R.string.cancel
+ ) { dialog: DialogInterface?, which: Int -> }
+ builder.setCancelable(false)
+ builder.show()
+ }
+
+ private fun sendReportEmail(media: Media, type: String) {
+ val technicalInfo = getTechInfo(media, type)
+
+ val feedbackIntent = Intent(Intent.ACTION_SENDTO)
+ feedbackIntent.setType("message/rfc822")
+ feedbackIntent.setData(Uri.parse("mailto:"))
+ feedbackIntent.putExtra(
+ Intent.EXTRA_EMAIL,
+ arrayOf(CommonsApplication.REPORT_EMAIL)
+ )
+ feedbackIntent.putExtra(
+ Intent.EXTRA_SUBJECT,
+ CommonsApplication.REPORT_EMAIL_SUBJECT
+ )
+ feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo)
+ try {
+ startActivity(feedbackIntent)
+ } catch (e: ActivityNotFoundException) {
+ Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun getTechInfo(media: Media, type: String): String {
+ val builder = StringBuilder()
+
+ builder.append("Report type: ")
+ .append(type)
+ .append("\n\n")
+
+ builder.append("Image that you want to report: ")
+ .append(media.imageUrl)
+ .append("\n\n")
+
+ builder.append("User that you want to report: ")
+ .append(media.user)
+ .append("\n\n")
+
+ if (sessionManager!!.userName != null) {
+ builder.append("Your username: ")
+ .append(sessionManager!!.userName)
+ .append("\n\n")
+ }
+
+ builder.append("Violation reason: ")
+ .append("\n")
+
+ builder.append("----------------------------------------------")
+ .append("\n")
+ .append("(please write reason here)")
+ .append("\n")
+ .append("----------------------------------------------")
+ .append("\n\n")
+ .append("Thank you for your report! Our team will investigate as soon as possible.")
+ .append("\n")
+ .append("Please note that images also have a `Nominate for deletion` button.")
+
+ return builder.toString()
+ }
+
+ /**
+ * Set the media as the device's wallpaper if the imageUrl is not null
+ * Fails silently if setting the wallpaper fails
+ * @param media
+ */
+ private fun setWallpaper(media: Media) {
+ if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
+ Timber.d("Media URL not present")
+ return
+ }
+ setWallpaperFromImageUrl(requireActivity(), media.imageUrl!!.toUri())
+ }
+
+ /**
+ * Set the media as user's leaderboard avatar
+ * @param media
+ */
+ private fun setAvatar(media: Media) {
+ if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
+ Timber.d("Media URL not present")
+ return
+ }
+ setAvatarFromImageUrl(
+ requireActivity(), media.imageUrl!!,
+ sessionManager!!.currentAccount!!.name,
+ okHttpJsonApiClient!!, Companion.compositeDisposable
+ )
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ if (!editable) { // Disable menu options for editable views
+ menu.clear() // see http://stackoverflow.com/a/8495697/17865
+ inflater.inflate(R.menu.fragment_image_detail, menu)
+ if (binding!!.mediaDetailsPager != null) {
+ val provider = mediaDetailProvider ?: return
+ val position = if (isFromFeaturedRootFragment) {
+ position
+ } else {
+ binding!!.mediaDetailsPager.currentItem
+ }
+
+ val m = provider.getMediaAtPosition(position)
+ if (m != null) {
+ // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
+ menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true)
+ menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true)
+ menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true)
+ menu.findItem(R.id.menu_download_current_image).setEnabled(true)
+ .setVisible(true)
+ menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true)
+ .setVisible(true)
+ menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true)
+ if (m.user != null) {
+ menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true)
+ }
+
+ try {
+ val mediaUrl = URL(m.imageUrl)
+ handleBackgroundColorMenuItems({
+ BitmapFactory.decodeStream(
+ mediaUrl.openConnection().getInputStream()
+ )
+ }, menu)
+ } catch (e: Exception) {
+ Timber.e("Cant detect media transparency")
+ }
+
+ // Initialize bookmark object
+ bookmark = Bookmark(
+ m.filename,
+ m.getAuthorOrUser(),
+ BookmarkPicturesContentProvider.uriForName(m.filename!!)
+ )
+ updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
+ val contributionState = provider.getContributionStateAt(position)
+ if (contributionState != null) {
+ when (contributionState) {
+ Contribution.STATE_FAILED, Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED -> {
+ menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_copy_link).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_share_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_download_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
+ .setVisible(false)
+ }
+
+ Contribution.STATE_COMPLETED -> {}
+ }
+ }
+ } else {
+ menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_copy_link).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_share_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_download_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
+ .setVisible(false)
+ menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
+ .setVisible(false)
+ }
+
+ if (!sessionManager!!.isUserLoggedIn) {
+ menu.findItem(R.id.menu_set_as_avatar).setVisible(false)
+ }
+ }
+ }
+ }
+
+ /**
+ * Decide wether or not we should display the background color menu items
+ * We display them if the image is transparent
+ * @param getBitmap
+ * @param menu
+ */
+ private fun handleBackgroundColorMenuItems(getBitmap: Callable, menu: Menu) {
+ Observable.fromCallable(
+ getBitmap
+ ).subscribeOn(Schedulers.newThread())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(Consumer { image: Bitmap ->
+ if (image.hasAlpha()) {
+ menu.findItem(R.id.menu_view_set_white_background).setVisible(true)
+ .setEnabled(true)
+ menu.findItem(R.id.menu_view_set_black_background).setVisible(true)
+ .setEnabled(true)
+ }
+ })
+ }
+
+ private fun updateBookmarkState(item: MenuItem) {
+ val isBookmarked = bookmarkDao!!.findBookmark(bookmark)
+ if (isBookmarked) {
+ if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
+ removedItems.remove(binding!!.mediaDetailsPager.currentItem)
+ }
+ } else {
+ if (!removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
+ removedItems.add(binding!!.mediaDetailsPager.currentItem)
+ }
+ }
+
+ item.setIcon(if (isBookmarked) {
+ R.drawable.menu_ic_round_star_filled_24px
+ } else {
+ R.drawable.menu_ic_round_star_border_24px
+ })
+ }
+
+ fun showImage(i: Int, isWikipediaButtonDisplayed: Boolean) {
+ this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed
+ setViewPagerCurrentItem(i)
+ }
+
+ fun showImage(i: Int) {
+ setViewPagerCurrentItem(i)
+ }
+
+ /**
+ * This function waits for the item to load then sets the item to current item
+ * @param position current item that to be shown
+ */
+ private fun setViewPagerCurrentItem(position: Int) {
+ val handler = Handler(Looper.getMainLooper())
+ val runnable: Runnable = object : Runnable {
+ override fun run() {
+ // Show the ProgressBar while waiting for the item to load
+ imageProgressBar!!.visibility = View.VISIBLE
+ // Check if the adapter has enough items loaded
+ if (adapter!!.count > position) {
+ // Set the current item in the ViewPager
+ binding!!.mediaDetailsPager.setCurrentItem(position, false)
+ // Hide the ProgressBar once the item is loaded
+ imageProgressBar!!.visibility = View.GONE
+ } else {
+ // If the item is not ready yet, post the Runnable again
+ handler.post(this)
+ }
+ }
+ }
+ // Start the Runnable
+ handler.post(runnable)
+ }
+
+ /**
+ * The method notify the viewpager that number of items have changed.
+ */
+ fun notifyDataSetChanged() {
+ if (null != adapter) {
+ adapter!!.notifyDataSetChanged()
+ }
+ }
+
+ override fun onPageScrolled(i: Int, v: Float, i2: Int) {
+ if (activity == null) {
+ Timber.d("Returning as activity is destroyed!")
+ return
+ }
+
+ requireActivity().invalidateOptionsMenu()
+ }
+
+ override fun onPageSelected(i: Int) {
+ }
+
+ override fun onPageScrollStateChanged(i: Int) {
+ }
+
+ fun onDataSetChanged() {
+ if (null != adapter) {
+ adapter!!.notifyDataSetChanged()
+ }
+ }
+
+ /**
+ * Called after the media is nominated for deletion
+ *
+ * @param index item position that has been nominated
+ */
+ override fun nominatingForDeletion(index: Int) {
+ mediaDetailProvider!!.refreshNominatedMedia(index)
+ }
+
+ companion object {
+ private val compositeDisposable = CompositeDisposable()
+
+ /**
+ * Use this factory method to create a new instance of this fragment using the provided
+ * parameters.
+ *
+ * This method will create a new instance of MediaDetailPagerFragment and the arguments will be
+ * saved to a bundle which will be later available in the [.onCreate]
+ * @param editable
+ * @param isFeaturedImage
+ * @return
+ */
+ @JvmStatic
+ fun newInstance(editable: Boolean, isFeaturedImage: Boolean): MediaDetailPagerFragment {
+ val mediaDetailPagerFragment = MediaDetailPagerFragment()
+ val args = Bundle()
+ args.putBoolean("is_editable", editable)
+ args.putBoolean("is_featured_image", isFeaturedImage)
+ mediaDetailPagerFragment.arguments = args
+ return mediaDetailPagerFragment
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt
new file mode 100644
index 000000000..591adfe75
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt
@@ -0,0 +1,14 @@
+package fr.free.nrw.commons.media
+
+import fr.free.nrw.commons.Media
+
+interface MediaDetailProvider {
+ fun getMediaAtPosition(i: Int): Media?
+
+ fun getTotalMediaCount(): Int
+
+ fun getContributionStateAt(position: Int): Int?
+
+ // Reload media detail fragment once media is nominated
+ fun refreshNominatedMedia(index: Int)
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
index ef0ef1f9c..d6c87410a 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
@@ -1,6 +1,6 @@
package fr.free.nrw.commons.media
-import fr.free.nrw.commons.OkHttpConnectionFactory.UnsuccessfulResponseInterceptor.SUPPRESS_ERROR_LOG_HEADER
+import fr.free.nrw.commons.SUPPRESS_ERROR_LOG_HEADER
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Single
import retrofit2.http.GET
@@ -186,13 +186,25 @@ interface MediaInterface {
): Single
companion object {
+ /**
+ * Retrieved thumbnail height will be about this tall, but must be at least this height.
+ * A larger number means higher thumbnail resolution but more network usage.
+ */
+ const val THUMB_HEIGHT_PX = 450
+
const val MEDIA_PARAMS =
- "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
+ "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlheight=" +
+ THUMB_HEIGHT_PX +
+ "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|" +
+ "ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
/**
* fetches category detail(title, hidden) for each category along with File information
*/
const val MEDIA_PARAMS_WITH_CATEGORY_DETAILS =
- "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
+ "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlheight=" +
+ THUMB_HEIGHT_PX +
+ "&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|" +
+ "DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java
deleted file mode 100644
index 28df3811a..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import fr.free.nrw.commons.wikidata.mwapi.MwResponse;
-
-public class MwParseResponse extends MwResponse {
- @Nullable
- private MwParseResult parse;
-
- @Nullable
- public MwParseResult parse() {
- return parse;
- }
-
- public boolean success() {
- return parse != null;
- }
-
- @VisibleForTesting
- protected void setParse(@Nullable MwParseResult parse) {
- this.parse = parse;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt
new file mode 100644
index 000000000..fc0282a9e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt
@@ -0,0 +1,17 @@
+package fr.free.nrw.commons.media
+
+import androidx.annotation.VisibleForTesting
+import fr.free.nrw.commons.wikidata.mwapi.MwResponse
+
+class MwParseResponse : MwResponse() {
+ private var parse: MwParseResult? = null
+
+ fun parse(): MwParseResult? = parse
+
+ fun success(): Boolean = parse != null
+
+ @VisibleForTesting
+ protected fun setParse(parse: MwParseResult?) {
+ this.parse = parse
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java
deleted file mode 100644
index edb7ff447..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import com.google.gson.annotations.SerializedName;
-
-public class MwParseResult {
- @SuppressWarnings("unused") private int pageid;
- @SuppressWarnings("unused") private int index;
- private MwParseText text;
-
- public String text() {
- return text.text;
- }
-
-
- public class MwParseText{
- @SerializedName("*") private String text;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
new file mode 100644
index 000000000..7aacdea09
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.media
+
+import com.google.gson.annotations.SerializedName
+
+class MwParseResult {
+ private val pageid = 0
+ private val index = 0
+ private val text: MwParseText? = null
+
+ fun text(): String? {
+ return text?.text
+ }
+
+ inner class MwParseText {
+ @SerializedName("*")
+ internal val text: String? = null
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt
index 291c834bd..a2f92c2e6 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt
@@ -281,6 +281,7 @@ class OkHttpJsonApiClient @Inject constructor(
FeedbackResponse::class.java
)
} catch (e: Exception) {
+ e.printStackTrace()
return@fromCallable FeedbackResponse(0, 0, 0, FeaturedImages(0, 0), 0, "")
}
}
@@ -531,40 +532,38 @@ ${"wd:" + place.wikiDataEntityId}"""
)
if (placeBindings != null) {
for ((item1, label, location, clas) in placeBindings) {
- if (item1 != null && label != null && clas != null) {
- val input = location.value
- val pattern = Pattern.compile(
- "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
- )
- val matcher = pattern.matcher(input)
+ val input = location.value
+ val pattern = Pattern.compile(
+ "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
+ )
+ val matcher = pattern.matcher(input)
- if (matcher.find()) {
- val longStr = matcher.group(1)
- val latStr = matcher.group(2)
- val itemUrl = item1.value
- val itemName = label.value.replace("&", "&")
- val itemLatitude = latStr
- val itemLongitude = longStr
- val itemClass = clas.value
+ if (matcher.find()) {
+ val longStr = matcher.group(1)
+ val latStr = matcher.group(2)
+ val itemUrl = item1.value
+ val itemName = label.value.replace("&", "&")
+ val itemLatitude = latStr
+ val itemLongitude = longStr
+ val itemClass = clas.value
- val formattedItemName =
- if (!itemClass.isEmpty())
- "$itemName ($itemClass)"
- else
- itemName
+ val formattedItemName =
+ if (!itemClass.isEmpty())
+ "$itemName ($itemClass)"
+ else
+ itemName
- val kmlEntry = ("""
-
- $formattedItemName
- $itemUrl
-
- $itemLongitude,$itemLatitude
-
- """)
- kmlString = kmlString + kmlEntry
- } else {
- Timber.e("No match found")
- }
+ val kmlEntry = ("""
+
+ $formattedItemName
+ $itemUrl
+
+ $itemLongitude,$itemLatitude
+
+ """)
+ kmlString = kmlString + kmlEntry
+ } else {
+ Timber.e("No match found")
}
}
}
@@ -589,37 +588,35 @@ ${"wd:" + place.wikiDataEntityId}"""
val placeBindings = runQuery(leftLatLng, rightLatLng)
if (placeBindings != null) {
for ((item1, label, location, clas) in placeBindings) {
- if (item1 != null && label != null && clas != null) {
- val input = location.value
- val pattern = Pattern.compile(
- "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
- )
- val matcher = pattern.matcher(input)
+ val input = location.value
+ val pattern = Pattern.compile(
+ "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
+ )
+ val matcher = pattern.matcher(input)
- if (matcher.find()) {
- val longStr = matcher.group(1)
- val latStr = matcher.group(2)
- val itemUrl = item1.value
- val itemName = label.value.replace("&", "&")
- val itemLatitude = latStr
- val itemLongitude = longStr
- val itemClass = clas.value
+ if (matcher.find()) {
+ val longStr = matcher.group(1)
+ val latStr = matcher.group(2)
+ val itemUrl = item1.value
+ val itemName = label.value.replace("&", "&")
+ val itemLatitude = latStr
+ val itemLongitude = longStr
+ val itemClass = clas.value
- val formattedItemName = if (!itemClass.isEmpty())
- "$itemName ($itemClass)"
- else
- itemName
+ val formattedItemName = if (!itemClass.isEmpty())
+ "$itemName ($itemClass)"
+ else
+ itemName
- val gpxEntry =
- ("""
-
- $itemName
- $itemUrl
- """)
- gpxString = gpxString + gpxEntry
- } else {
- Timber.e("No match found")
- }
+ val gpxEntry =
+ ("""
+
+ $itemName
+ $itemUrl
+ """)
+ gpxString = gpxString + gpxEntry
+ } else {
+ Timber.e("No match found")
}
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt
index 3f7a196fe..a4f08f241 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt
@@ -18,7 +18,6 @@ import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.WelcomeActivity
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding
import fr.free.nrw.commons.di.ApplicationlessInjection
@@ -32,6 +31,7 @@ import fr.free.nrw.commons.logging.CommonsLogSender
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewActivity
import fr.free.nrw.commons.settings.SettingsActivity
+import fr.free.nrw.commons.startWelcome
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
@@ -114,13 +114,13 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
val level = store.getString("userAchievementsLevel", "0")
if (level == "0"){
binding?.moreProfile?.text = getString(
- R.string.profileLevel,
+ R.string.profile_withoutLevel,
getUserName(),
getString(R.string.see_your_achievements) // Second argument
)
} else {
binding?.moreProfile?.text = getString(
- R.string.profileLevel,
+ R.string.profile_withLevel,
getUserName(),
level
)
@@ -241,7 +241,7 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
}
fun onTutorialClicked() {
- WelcomeActivity.startYourself(requireActivity())
+ requireContext().startWelcome()
}
fun onSettingsClicked() {
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 a83d49f75..714cd388f 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
@@ -16,7 +16,7 @@ import fr.free.nrw.commons.nearby.model.BottomSheetItem
/**
* RecyclerView Adapter for displaying items in a bottom sheet.
*
- * @property context The context used for inflating layout resources.
+ * @param context The context used for inflating layout resources.
* @property itemList The list of BottomSheetItem objects to display.
* @constructor Creates an instance of BottomSheetAdapter.
*/
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java b/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java
index db2c1f5d9..323f9756f 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java
@@ -44,7 +44,7 @@ public class CheckBoxTriStates extends AppCompatCheckBox {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
switch (state) {
case UNKNOWN:
- setState(UNCHECKED);;
+ setState(UNCHECKED);
break;
case UNCHECKED:
setState(CHECKED);
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java
index b5f760c9f..53e9970a6 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java
@@ -91,6 +91,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
label.setSelected(!label.isSelected());
holder.placeTypeLayout.setSelected(label.isSelected());
+ NearbyFilterState.setSelectedLabels(new ArrayList<>(selectedLabels));
callback.filterByMarkerType(selectedLabels, 0, false, false);
});
}
@@ -152,6 +153,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
label.setSelected(false);
selectedLabels.remove(label);
}
+ NearbyFilterState.setSelectedLabels(new ArrayList<>(selectedLabels));
notifyDataSetChanged();
}
@@ -163,6 +165,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
selectedLabels.add(label);
}
}
+ NearbyFilterState.setSelectedLabels(new ArrayList<>(selectedLabels));
notifyDataSetChanged();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java
index d3ece9bfa..d0aec96af 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java
@@ -9,7 +9,7 @@ public class NearbyFilterState {
private int checkBoxTriState;
private ArrayList selectedLabels;
- private static NearbyFilterState nearbyFılterStateInstance;
+ private static NearbyFilterState nearbyFilterStateInstance;
/**
* Define initial filter values here
@@ -23,10 +23,10 @@ public class NearbyFilterState {
}
public static NearbyFilterState getInstance() {
- if (nearbyFılterStateInstance == null) {
- nearbyFılterStateInstance = new NearbyFilterState();
+ if (nearbyFilterStateInstance == null) {
+ nearbyFilterStateInstance = new NearbyFilterState();
}
- return nearbyFılterStateInstance;
+ return nearbyFilterStateInstance;
}
public static void setSelectedLabels(ArrayList selectedLabels) {
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java
index caae8ee45..3bb2f549f 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java
@@ -1,10 +1,14 @@
package fr.free.nrw.commons.nearby;
+import static java.util.Collections.emptyList;
+
import android.location.Location;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.nearby.model.NearbyQueryParams;
+import fr.free.nrw.commons.nearby.model.NearbyQueryParams.Radial;
+import fr.free.nrw.commons.nearby.model.NearbyQueryParams.Rectangular;
import java.io.IOException;
-import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
@@ -46,13 +50,14 @@ public class NearbyPlaces {
* @param customQuery
* @return list of places obtained
*/
+ @NonNull
List radiusExpander(final LatLng currentLatLng, final String lang,
final boolean returnClosestResult, @Nullable final String customQuery) throws Exception {
final int minResults;
final double maxRadius;
- List places = Collections.emptyList();
+ List places = emptyList();
// If returnClosestResult is true, then this means that we are trying to get closest point
// to use in cardView in Contributions fragment
@@ -113,6 +118,7 @@ public class NearbyPlaces {
* @return A list of places obtained from the Wikidata query.
* @throws Exception If an error occurs during the retrieval process.
*/
+ @NonNull
public List getFromWikidataQuery(
final fr.free.nrw.commons.location.LatLng centerPoint,
final fr.free.nrw.commons.location.LatLng screenTopRight,
@@ -120,11 +126,11 @@ public class NearbyPlaces {
final boolean shouldQueryForMonuments,
@Nullable final String customQuery) throws Exception {
if (customQuery != null) {
- return okHttpJsonApiClient
- .getNearbyPlaces(
- new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
+ final List nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
+ new Rectangular(screenTopRight, screenBottomLeft), lang,
shouldQueryForMonuments,
customQuery);
+ return nearbyPlaces != null ? nearbyPlaces : emptyList();
}
final int lowerLimit = 1000, upperLimit = 1500;
@@ -141,9 +147,10 @@ public class NearbyPlaces {
final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft));
if (itemCount < upperLimit) {
- return okHttpJsonApiClient.getNearbyPlaces(
- new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
+ final List nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
+ new Rectangular(screenTopRight, screenBottomLeft), lang,
shouldQueryForMonuments, null);
+ return nearbyPlaces != null ? nearbyPlaces : emptyList();
}
}
@@ -175,9 +182,10 @@ public class NearbyPlaces {
maxRadius = targetRadius - 1;
}
}
- return okHttpJsonApiClient.getNearbyPlaces(
- new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
+ final List nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
+ new Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
null);
+ return nearbyPlaces != null ? nearbyPlaces : emptyList();
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java
index fc01585ce..93bcba717 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java
@@ -105,9 +105,6 @@ public class Sitelinks implements Parcelable {
private String commonsLink;
private String wikipediaLink;
- public Builder() {
- }
-
public Sitelinks.Builder setWikipediaLink(String link) {
this.wikipediaLink = link;
return this;
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
index e5196bee8..1775401dd 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
@@ -63,7 +63,10 @@ class WikidataFeedback : BaseActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
binding.appCompatButton.setOnClickListener {
- var desc = findViewById(binding.radioGroup.checkedRadioButtonId).text
+ var desc = when (binding.radioGroup.checkedRadioButtonId) {
+ R.id.radioButton2 -> getString(R.string.is_at_a_different_place_wikidata, place)
+ else -> findViewById(binding.radioGroup.checkedRadioButtonId).text
+ }
var det = binding.detailsEditText.text.toString()
if (binding.radioGroup.checkedRadioButtonId == R.id.radioButton3 && binding.detailsEditText.text.isNullOrEmpty()) {
Toast
@@ -103,4 +106,4 @@ class WikidataFeedback : BaseActivity() {
onBackPressed()
return true
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt
index 202f2c305..5f4d0ab13 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt
@@ -10,12 +10,13 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.utils.ActivityUtils
+import fr.free.nrw.commons.utils.handleGeoCoordinates
+import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.wikidata.WikidataConstants
import timber.log.Timber
import javax.inject.Inject
@@ -104,7 +105,7 @@ class CommonPlaceClickActions
fun onDirectionsClicked(): (Place) -> Unit =
{
- Utils.handleGeoCoordinates(activity, it.getLocation())
+ handleGeoCoordinates(activity, it.getLocation())
}
private fun storeSharedPrefs(selectedPlace: Place) {
@@ -113,7 +114,7 @@ class CommonPlaceClickActions
}
private fun openWebView(link: Uri): Boolean {
- Utils.handleWebUrl(activity, link)
+ handleWebUrl(activity, link)
return true
}
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 25baf3a92..3e6e71511 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
@@ -58,7 +58,6 @@ 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
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.contributions.MainActivity
@@ -75,7 +74,7 @@ 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.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
import fr.free.nrw.commons.nearby.BottomSheetAdapter
import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener
@@ -104,6 +103,10 @@ import fr.free.nrw.commons.utils.NearbyFABUtils.removeAnchorFromFAB
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
import fr.free.nrw.commons.utils.SystemThemeUtils
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
+import fr.free.nrw.commons.utils.copyToClipboard
+import fr.free.nrw.commons.utils.handleGeoCoordinates
+import fr.free.nrw.commons.utils.handleWebUrl
+import fr.free.nrw.commons.utils.isMonumentsEnabled
import fr.free.nrw.commons.wikidata.WikidataConstants
import fr.free.nrw.commons.wikidata.WikidataEditListener
import fr.free.nrw.commons.wikidata.WikidataEditListener.WikidataP18EditListener
@@ -122,6 +125,7 @@ import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.MapEventsOverlay
import org.osmdroid.views.overlay.Marker
+import org.osmdroid.views.overlay.Overlay
import org.osmdroid.views.overlay.ScaleBarOverlay
import org.osmdroid.views.overlay.ScaleDiskOverlay
import org.osmdroid.views.overlay.TilesOverlay
@@ -138,7 +142,6 @@ 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
@@ -148,7 +151,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
LocationUpdateListener,
LocationPermissionCallback,
ItemClickListener,
- MediaDetailPagerFragment.MediaDetailProvider {
+ MediaDetailProvider {
var binding: FragmentNearbyParentBinding? = null
val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver {
@@ -266,6 +269,9 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
private var dataList: MutableList? = null
private var bottomSheetAdapter: BottomSheetAdapter? = null
+ private var userLocationOverlay: Overlay? = null
+ private var userLocationErrorOverlay: Overlay? = null
+
private val galleryPickLauncherForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
controller?.handleActivityResultWithCallback(
@@ -462,7 +468,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
}
}
_isDarkTheme = systemThemeUtils?.isDeviceInNightMode() == true
- if (Utils.isMonumentsEnabled(Date())) {
+ if (isMonumentsEnabled) {
binding?.rlContainerWlmMonthMessage?.visibility = View.VISIBLE
} else {
binding?.rlContainerWlmMonthMessage?.visibility = View.GONE
@@ -723,7 +729,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
val targetP = GeoPoint(target.latitude, target.longitude)
mapCenter = targetP
binding?.map?.controller?.setCenter(targetP)
- recenterMarkerToPosition(targetP)
+ updateUserLocationOverlays(targetP, true)
if (!isCameFromExploreMap()) {
moveCameraToPosition(targetP)
}
@@ -831,7 +837,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
loadAnimations()
setBottomSheetCallbacks()
addActionToTitle()
- if (!Utils.isMonumentsEnabled(Date())) {
+ if (!isMonumentsEnabled) {
NearbyFilterState.setWlmSelected(false)
}
}
@@ -875,6 +881,12 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
fun initNearbyFilter() {
binding!!.nearbyFilterList.root.visibility = View.GONE
hideBottomSheet()
+ binding!!.nearbyFilter.searchViewLayout.searchView.apply {
+ setIconifiedByDefault(false)
+ isIconified = false
+ setQuery("", false)
+ clearFocus()
+ }
binding!!.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener { v, hasFocus ->
setLayoutHeightAlignedToWidth(
1.25,
@@ -918,6 +930,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
return _isDarkTheme
}
})
+ restoreStoredFilterSelection()
binding!!.nearbyFilterList.root
.layoutParams.width = getScreenWidth(
requireActivity(),
@@ -936,6 +949,22 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
})
}
+ private fun restoreStoredFilterSelection() {
+ val adapter = nearbyFilterSearchRecyclerViewAdapter ?: return
+ val savedLabels = ArrayList(NearbyFilterState.getInstance().selectedLabels)
+ adapter.selectedLabels.clear()
+ val savedSet = savedLabels.toSet()
+ Label.valuesAsList().forEach { label ->
+ val isSelected = savedSet.contains(label)
+ label.setSelected(isSelected)
+ if (isSelected) {
+ adapter.selectedLabels.add(label)
+ }
+ }
+ NearbyFilterState.setSelectedLabels(ArrayList(adapter.selectedLabels))
+ adapter.notifyDataSetChanged()
+ }
+
override fun setCheckBoxAction() {
binding!!.nearbyFilterList.checkboxTriStates.addAction()
binding!!.nearbyFilterList.checkboxTriStates.state = CheckBoxTriStates.UNKNOWN
@@ -973,7 +1002,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
} else if (bottomSheetDetailsBehavior!!.state
== BottomSheetBehavior.STATE_EXPANDED
) {
- bottomSheetDetailsBehavior!!.state = BottomSheetBehavior.STATE_COLLAPSED
+ bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_COLLAPSED)
}
}
@@ -1012,11 +1041,10 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
*/
private fun addActionToTitle() {
binding!!.bottomSheetDetails.title.setOnLongClickListener { view ->
- Utils.copy(
- "place", binding!!.bottomSheetDetails.title.text.toString(),
- context
+ requireContext().copyToClipboard(
+ "place", binding!!.bottomSheetDetails.title.text.toString()
)
- Toast.makeText(context, fr.free.nrw.commons.R.string.text_copy, Toast.LENGTH_SHORT)
+ Toast.makeText(requireContext(), fr.free.nrw.commons.R.string.text_copy, Toast.LENGTH_SHORT)
.show()
true
}
@@ -1064,7 +1092,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
override fun updateListFragment(placeList: List) {
adapter!!.clear()
- adapter!!.items = placeList
+ adapter!!.items = placeList.filter{ it.name.isNotEmpty() }
binding!!.bottomSheetNearby.noResultsMessage.visibility =
if (placeList.isEmpty()) View.VISIBLE else View.GONE
}
@@ -1575,7 +1603,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
searchLatLng,
false,
true,
- Utils.isMonumentsEnabled(Date()),
+ isMonumentsEnabled,
customQuery
)
}
@@ -1628,7 +1656,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
searchLatLng,
false,
true,
- Utils.isMonumentsEnabled(Date()),
+ isMonumentsEnabled,
customQuery
)
}
@@ -1756,9 +1784,9 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
override fun animateFABs() {
if (binding!!.fabPlus.isShown) {
if (isFABsExpanded) {
- collapseFABs(isFABsExpanded)
+ collapseFABs(true)
} else {
- expandFABs(isFABsExpanded)
+ expandFABs(false)
}
}
}
@@ -1862,6 +1890,8 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
lastKnownLocation = latLng
NearbyController.currentLocation = lastKnownLocation
presenter!!.updateMapAndList(locationChangeType)
+
+ updateUserLocationOverlays(GeoPoint(latLng.latitude, latLng.longitude), true)
}
override fun onLocationChangedSignificantly(latLng: LatLng) {
@@ -2013,17 +2043,17 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
if (place.exists && place.pic.trim { it <= ' ' }.isEmpty()) {
shouldUpdateMarker = true
}
- } else if (displayExists && !displayNeedsPhoto) {
+ } else if (displayExists) {
// Exists and all included needs and doesn't needs photo
if (place.exists) {
shouldUpdateMarker = true
}
- } else if (!displayExists && displayNeedsPhoto) {
+ } else if (displayNeedsPhoto) {
// All and only needs photo
if (place.pic.trim { it <= ' ' }.isEmpty()) {
shouldUpdateMarker = true
}
- } else if (!displayExists && !displayNeedsPhoto) {
+ } else {
// all
shouldUpdateMarker = true
}
@@ -2456,9 +2486,11 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
Timber.d("Gallery button tapped. Place: %s", selectedPlace.toString())
storeSharedPrefs(selectedPlace!!)
activity?.let {
+ // Pass singleSelection = true for Nearby flow
controller!!.initiateCustomGalleryPickWithPermission(
it,
- customSelectorLauncherForResult
+ customSelectorLauncherForResult,
+ singleSelection = true
)
}
}
@@ -2641,43 +2673,14 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
*/
override fun clearAllMarkers() {
binding!!.map.overlayManager.clear()
- binding!!.map.invalidate()
- val geoPoint = mapCenter
- if (geoPoint != null) {
- val diskOverlay =
- ScaleDiskOverlay(
- this.context,
- geoPoint, 2000, UnitOfMeasure.foot
- )
- val circlePaint = Paint()
- circlePaint.color = Color.rgb(128, 128, 128)
- circlePaint.style = Paint.Style.STROKE
- circlePaint.strokeWidth = 2f
- diskOverlay.setCirclePaint2(circlePaint)
- val diskPaint = Paint()
- diskPaint.color = Color.argb(40, 128, 128, 128)
- diskPaint.style = Paint.Style.FILL_AND_STROKE
- diskOverlay.setCirclePaint1(diskPaint)
- diskOverlay.setDisplaySizeMin(900)
- diskOverlay.setDisplaySizeMax(1700)
- binding!!.map.overlays.add(diskOverlay)
- val startMarker = Marker(
- binding!!.map
- )
- startMarker.position = geoPoint
- startMarker.setAnchor(
- Marker.ANCHOR_CENTER,
- Marker.ANCHOR_BOTTOM
- )
- startMarker.icon =
- getDrawable(
- this.requireContext(),
- fr.free.nrw.commons.R.drawable.current_location_marker
- )
- startMarker.title = "Your Location"
- startMarker.textLabelFontSize = 24
- binding!!.map.overlays.add(startMarker)
+
+ var geoPoint = mapCenter
+ val lastLatLng = locationManager.getLastLocation()
+ if (lastLatLng != null) {
+ geoPoint = GeoPoint(lastLatLng.latitude, lastLatLng.longitude)
}
+ updateUserLocationOverlays(geoPoint, false)
+
val scaleBarOverlay = ScaleBarOverlay(binding!!.map)
scaleBarOverlay.setScaleBarOffset(15, 25)
val barPaint = Paint()
@@ -2687,6 +2690,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
binding!!.map.overlays.add(scaleBarOverlay)
binding!!.map.overlays.add(mapEventsOverlay)
binding!!.map.setMultiTouchControls(true)
+ binding!!.map.invalidate()
}
/**
@@ -2697,45 +2701,149 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
private fun recenterMarkerToPosition(geoPoint: GeoPoint?) {
geoPoint?.let {
binding?.map?.controller?.setCenter(it)
- val overlays = binding?.map?.overlays ?: return@let
- // Remove markers and disks using index-based removal
- var i = 0
- while (i < overlays.size) {
- when (overlays[i]) {
- is Marker, is ScaleDiskOverlay -> overlays.removeAt(i)
- else -> i++
- }
- }
-
- // Add disk overlay
- ScaleDiskOverlay(context, it, 2000, UnitOfMeasure.foot).apply {
- setCirclePaint2(Paint().apply {
- color = Color.rgb(128, 128, 128)
- style = Paint.Style.STROKE
- strokeWidth = 2f
- })
- setCirclePaint1(Paint().apply {
- color = Color.argb(40, 128, 128, 128)
- style = Paint.Style.FILL_AND_STROKE
- })
- setDisplaySizeMin(900)
- setDisplaySizeMax(1700)
- overlays.add(this)
- }
-
- // Add marker
- Marker(binding?.map).apply {
- position = it
- setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
- icon = getDrawable(context, R.drawable.current_location_marker)
- title = "Your Location"
- textLabelFontSize = 24
- overlays.add(this)
- }
+ updateUserLocationOverlays(it, true);
}
}
+ /**
+ * Updates the user current location overlays (both the location and error overlays) by
+ * replacing any existing location overlays with new overlays at the given GeoPoint. If there
+ * are no existing location and error overlays, then new overlays are added.
+ *
+ * @param geoPoint The GeoPoint representing the user's current location.
+ * @param invalidate If true, the map overlays will be invalidated after the user
+ * location overlays are updated/added. If false, the overlays will not be invalidated.
+ */
+ private fun updateUserLocationOverlays(geoPoint: GeoPoint?, invalidate: Boolean) {
+ geoPoint?.let{
+ updateUserLocationOverlay(geoPoint)
+ updateUserLocationErrorOverlay(geoPoint)
+ }
+
+ if (invalidate) {
+ binding!!.map.invalidate()
+ }
+ }
+
+ /**
+ * Updates the user location error overlay by either replacing it with or adding a new one.
+ *
+ * If the user location error overlay is null, the new overlay is added. If the
+ * overlay is not null, it is replaced by the new overlay.
+ *
+ * @param geoPoint The GeoPoint representing the user's location
+ */
+ private fun updateUserLocationErrorOverlay(geoPoint: GeoPoint) {
+ val overlays = binding?.map?.overlays ?: return
+
+ // Multiply accuracy by 2 to get 95% confidence interval
+ val accuracy = getCurrentLocationAccuracy() * 2
+ val overlay = createCurrentLocationErrorOverlay(this.context, geoPoint,
+ (accuracy).toInt(), UnitOfMeasure.meter)
+
+ val index = overlays.indexOf(userLocationErrorOverlay)
+
+ if (userLocationErrorOverlay == null || index == -1) {
+ overlays.add(overlay)
+ } else {
+ overlays[index] = overlay
+ }
+
+ userLocationErrorOverlay = overlay
+ }
+
+ /**
+ * Updates the user location overlay by either replacing it with or adding a new one.
+ *
+ * If the user location overlay is null, the new overlay is added. If the
+ * overlay is not null, it is replaced by the new overlay.
+ *
+ * @param geoPoint The GeoPoint representing the user's location
+ */
+ private fun updateUserLocationOverlay(geoPoint: GeoPoint) {
+ val overlays = binding?.map?.overlays ?: return
+
+ val overlay = createCurrentLocationOverlay(geoPoint)
+
+ val index = overlays.indexOf(userLocationOverlay)
+
+ if (userLocationOverlay == null || index == -1) {
+ overlays.add(overlay)
+ } else {
+ overlays[index] = overlay
+ }
+
+ userLocationOverlay = overlay
+ }
+
+ /**
+ * @return The accuracy of the current location with a confidence at the 68th percentile.
+ * Units are in meters. Returning 0 may indicate failure.
+ */
+ private fun getCurrentLocationAccuracy(): Float {
+ var accuracy = 0f
+ val lastLocation = locationManager.getLastLocation()
+ if (lastLocation != null) {
+ accuracy = lastLocation.accuracy
+ }
+
+ return accuracy
+ }
+
+ /**
+ * Creates the current location overlay
+ *
+ * @param geoPoint The GeoPoint where the current location overlay will be placed.
+ *
+ * @return The current location overlay as a Marker
+ */
+ private fun createCurrentLocationOverlay(geoPoint: GeoPoint): Marker {
+ val currentLocationOverlay = Marker(
+ binding!!.map
+ )
+ currentLocationOverlay.position = geoPoint
+ currentLocationOverlay.icon =
+ getDrawable(
+ this.requireContext(),
+ fr.free.nrw.commons.R.drawable.current_location_marker
+ )
+ currentLocationOverlay.title = "Your Location"
+ currentLocationOverlay.textLabelFontSize = 24
+ currentLocationOverlay.setAnchor(0.5f, 0.5f)
+
+ return currentLocationOverlay
+ }
+
+ /**
+ * Creates the location error overlay to show the user how accurate the current location
+ * overlay is. The edge of the disk is the 95% confidence interval.
+ *
+ * @param context The Android context
+ * @param point The user's location as a GeoPoint
+ * @param value The radius of the disk
+ * @param unitOfMeasure The unit of measurement of the value/disk radius.
+ *
+ * @return The location error overlay as a ScaleDiskOverlay.
+ */
+ private fun createCurrentLocationErrorOverlay(context: Context?, point: GeoPoint, value: Int,
+ unitOfMeasure: UnitOfMeasure): ScaleDiskOverlay {
+ val scaleDisk = ScaleDiskOverlay(context, point, value, unitOfMeasure)
+
+ scaleDisk.setCirclePaint2(Paint().apply {
+ color = Color.rgb(128, 128, 128)
+ style = Paint.Style.STROKE
+ strokeWidth = 2f
+ })
+
+ scaleDisk.setCirclePaint1(Paint().apply {
+ color = Color.argb(40, 128, 128, 128)
+ style = Paint.Style.FILL_AND_STROKE
+ })
+
+ return scaleDisk
+ }
+
private fun moveCameraToPosition(geoPoint: GeoPoint) {
binding!!.map.controller.animateTo(geoPoint)
}
@@ -2769,14 +2877,14 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
R.drawable.ic_directions_black_24dp -> {
selectedPlace?.let {
- Utils.handleGeoCoordinates(this.context, it.getLocation())
+ handleGeoCoordinates(requireContext(), it.getLocation())
binding?.map?.zoomLevelDouble ?: 0.0
}
}
R.drawable.ic_wikidata_logo_24dp -> {
selectedPlace?.siteLinks?.wikidataLink?.let {
- Utils.handleWebUrl(this.context, it)
+ handleWebUrl(requireContext(), it)
}
}
@@ -2794,13 +2902,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
R.drawable.ic_wikipedia_logo_24dp -> {
selectedPlace?.siteLinks?.wikipediaLink?.let {
- Utils.handleWebUrl(this.context, it)
+ handleWebUrl(requireContext(), it)
}
}
R.drawable.ic_commons_icon_vector -> {
selectedPlace?.siteLinks?.commonsLink?.let {
- Utils.handleWebUrl(this.context, it)
+ handleWebUrl(requireContext(), it)
}
}
@@ -2902,4 +3010,4 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
return input.contains("(") || input.contains(")")
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
index c39d8901d..1d5d7bd80 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
@@ -7,7 +7,8 @@ class NearbyResultItem(
private val wikipediaArticle: ResultTuple?,
private val commonsArticle: ResultTuple?,
private val location: ResultTuple?,
- private val label: ResultTuple?,
+ @field:SerializedName("label") private val label: ResultTuple?,
+ @field:SerializedName("itemLabel") private val itemLabel: ResultTuple?,
@field:SerializedName("streetAddress") private val address: ResultTuple?,
private val icon: ResultTuple?,
@field:SerializedName("class") private val className: ResultTuple?,
@@ -29,7 +30,15 @@ class NearbyResultItem(
fun getLocation(): ResultTuple = location ?: ResultTuple()
- fun getLabel(): ResultTuple = label ?: ResultTuple()
+ /**
+ * Returns label for display (pins, popup), using fallback to itemLabel if needed.
+ */
+ fun getLabel(): ResultTuple = label ?: itemLabel ?: ResultTuple()
+
+ /**
+ * Returns only the original label field, for Wikidata edits.
+ */
+ fun getOriginalLabel(): ResultTuple = label ?: ResultTuple()
fun getIcon(): ResultTuple = icon ?: ResultTuple()
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 b4639b14a..6ec1064be 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
@@ -351,6 +351,7 @@ class NearbyParentFragmentPresenter
pic = repoPlace.pic ?: ""
exists = repoPlace.exists ?: true
longDescription = repoPlace.longDescription ?: ""
+ language = repoPlace.language
}
} else {
indicesToUpdate.add(i)
diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt
index 1547f89ad..4a43bf470 100644
--- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt
@@ -8,20 +8,23 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import androidx.core.view.ViewGroupCompat
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.databinding.ActivityNotificationBinding
import fr.free.nrw.commons.notification.models.Notification
import fr.free.nrw.commons.notification.models.NotificationType
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
import fr.free.nrw.commons.utils.NetworkUtils
import fr.free.nrw.commons.utils.ViewUtil
+import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
+import fr.free.nrw.commons.utils.handleWebUrl
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
@@ -56,6 +59,9 @@ class NotificationActivity : BaseActivity() {
super.onCreate(savedInstanceState)
isRead = intent.getStringExtra("title") == "read"
binding = ActivityNotificationBinding.inflate(layoutInflater)
+ ViewGroupCompat.installCompatInsetsDispatch(binding.root)
+ applyEdgeToEdgeTopInsets(binding.toolbar.toolbar)
+ binding.listView.applyEdgeToEdgeBottomPaddingInsets()
setContentView(binding.root)
mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag(
tagNotificationWorkerFragment
@@ -197,7 +203,7 @@ class NotificationActivity : BaseActivity() {
private fun handleUrl(url: String?) {
if (url.isNullOrEmpty()) return
- Utils.handleWebUrl(this, Uri.parse(url))
+ handleWebUrl(this, Uri.parse(url))
}
private fun setItems(notificationList: List?) {
diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
index 164842c9a..8567d37ae 100644
--- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
@@ -3,16 +3,17 @@ package fr.free.nrw.commons.profile
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
-import android.net.Uri
import android.os.Bundle
import android.util.Log
-import android.view.*
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.FileProvider
+import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.ContributionsFragment
@@ -20,11 +21,13 @@ import fr.free.nrw.commons.databinding.ActivityProfileBinding
import fr.free.nrw.commons.profile.achievements.AchievementsFragment
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.utils.DialogUtil
import java.io.File
import java.io.FileOutputStream
-import java.util.*
+import java.util.Locale
import javax.inject.Inject
+import timber.log.Timber
/**
* This activity will set two tabs, achievements and
@@ -45,7 +48,7 @@ class ProfileActivity : BaseActivity() {
private var contributionsFragment: ContributionsFragment? = null
fun setScroll(canScroll: Boolean) {
- binding.viewPager.setCanScroll(canScroll)
+ binding.viewPager.canScroll = canScroll
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -60,6 +63,7 @@ class ProfileActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
+ applyEdgeToEdgeAllInsets(binding.root)
setContentView(binding.root)
setSupportActionBar(binding.toolbarBinding.toolbar)
@@ -71,7 +75,7 @@ class ProfileActivity : BaseActivity() {
title = userName
shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false)
- viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
+ viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
setTabs()
@@ -83,39 +87,23 @@ class ProfileActivity : BaseActivity() {
}
private fun setTabs() {
- val fragmentList = mutableListOf()
- val titleList = mutableListOf()
-
- // Add Achievements tab
achievementsFragment = AchievementsFragment().apply {
- arguments = Bundle().apply {
- putString(KEY_USERNAME, userName)
- }
+ arguments = bundleOf(KEY_USERNAME to userName)
}
- fragmentList.add(achievementsFragment)
- titleList.add(resources.getString(R.string.achievements_tab_title).uppercase())
- // Add Leaderboard tab
leaderboardFragment = LeaderboardFragment().apply {
- arguments = Bundle().apply {
- putString(KEY_USERNAME, userName)
- }
+ arguments = bundleOf(KEY_USERNAME to userName)
}
- fragmentList.add(leaderboardFragment)
- titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT))
- // Add Contributions tab
contributionsFragment = ContributionsFragment().apply {
- arguments = Bundle().apply {
- putString(KEY_USERNAME, userName)
- }
- }
- contributionsFragment?.let {
- fragmentList.add(it)
- titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT))
+ arguments = bundleOf(KEY_USERNAME to userName)
}
- viewPagerAdapter.setTabData(fragmentList, titleList)
+ viewPagerAdapter.setTabs(
+ R.string.achievements_tab_title to achievementsFragment,
+ R.string.leaderboard_tab_title to leaderboardFragment,
+ R.string.contributions_fragment to contributionsFragment!!
+ )
viewPagerAdapter.notifyDataSetChanged()
}
@@ -133,9 +121,9 @@ class ProfileActivity : BaseActivity() {
return when (item.itemId) {
R.id.share_app_icon -> {
val rootView = window.decorView.findViewById(android.R.id.content)
- val screenShot = Utils.getScreenShot(rootView)
+ val screenShot = getScreenShot(rootView)
if (screenShot == null) {
- Log.e("ERROR", "ScreenShot is null")
+ Timber.e("ScreenShot is null")
return false
}
showAlert(screenShot)
@@ -212,6 +200,24 @@ class ProfileActivity : BaseActivity() {
binding.tabLayout.visibility = if (isVisible) View.VISIBLE else View.GONE
}
+ /**
+ * To take screenshot of the screen and return it in Bitmap format
+ *
+ * @param view
+ * @return
+ */
+ fun getScreenShot(view: View): Bitmap? {
+ val screenView = view.rootView
+ screenView.isDrawingCacheEnabled = true
+ val drawingCache = screenView.drawingCache
+ if (drawingCache != null) {
+ val bitmap = Bitmap.createBitmap(drawingCache)
+ screenView.isDrawingCacheEnabled = false
+ return bitmap
+ }
+ return null
+ }
+
companion object {
const val KEY_USERNAME = "username"
const val KEY_SHOULD_SHOW_CONTRIBUTIONS = "shouldShowContributions"
diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt
index af07423eb..8f23674ca 100644
--- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt
@@ -15,7 +15,6 @@ import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.databinding.FragmentAchievementsBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
@@ -27,6 +26,7 @@ import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import fr.free.nrw.commons.utils.ViewUtil.showDismissibleSnackBar
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
+import fr.free.nrw.commons.utils.handleWebUrl
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.apache.commons.lang3.StringUtils
@@ -389,7 +389,7 @@ class AchievementsFragment : CommonsDaggerSupportFragment(){
* @param badgeTextColor The badge text color. Default is R.attr.colorPrimary
* @param badgeGravity The position of the badge [TOP_END,TOP_START,BOTTOM_END,BOTTOM_START]. Default is TOP_END
* @return if the number is 0, then it will not create badge for it and hide the view
- * @see https://developer.android.com/reference/com/google/android/material/badge/BadgeDrawable
+ * @see BadgeDrawable (Android Developer)
*/
private fun showBadgesWithCount(
@@ -524,7 +524,7 @@ class AchievementsFragment : CommonsDaggerSupportFragment(){
getString(R.string.ok),
getString(R.string.read_help_link),
{},
- { Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) },
+ { handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) },
null
)
}
diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt
index e77c24c8d..5dcdf283b 100644
--- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt
@@ -311,7 +311,7 @@ class LeaderboardFragment : CommonsDaggerSupportFragment() {
}
private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener {
- override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) =
+ override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) =
handler()
override fun onNothingSelected(p0: AdapterView<*>?) = Unit
diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt
index e65b819e5..11fd1e6a6 100644
--- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt
@@ -3,9 +3,11 @@ package fr.free.nrw.commons.quiz
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.facebook.drawee.drawable.ProgressBarDrawable
@@ -15,6 +17,7 @@ import fr.free.nrw.commons.databinding.ActivityQuizBinding
import java.util.ArrayList
import fr.free.nrw.commons.R
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
class QuizActivity : AppCompatActivity() {
@@ -37,7 +40,11 @@ class QuizActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
binding = ActivityQuizBinding.inflate(layoutInflater)
+ applyEdgeToEdgeAllInsets(binding.root)
+ WindowCompat.getInsetsController(window, window.decorView)
+ .isAppearanceLightStatusBars = true
setContentView(binding.root)
quizController.initialize(this)
diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt
index 0183056a6..5362f1542 100644
--- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt
+++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt
@@ -151,7 +151,7 @@ class QuizChecker @Inject constructor(
activity.getString(R.string.quiz),
activity.getString(R.string.quiz_alert_message, revertPercentageForMessage),
activity.getString(R.string.about_translate_proceed),
- activity.getString(android.R.string.cancel),
+ activity.getString(R.string.cancel),
{ startQuizActivity(activity) },
null
)
diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt
index 0282da190..15884146d 100644
--- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt
@@ -12,9 +12,11 @@ import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
+import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
import fr.free.nrw.commons.databinding.ActivityQuizResultBinding
import java.io.File
@@ -22,6 +24,7 @@ import java.io.FileOutputStream
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.MainActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
/**
@@ -35,7 +38,11 @@ class QuizResultActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
binding = ActivityQuizResultBinding.inflate(layoutInflater)
+ applyEdgeToEdgeAllInsets(binding!!.root)
+ WindowCompat.getInsetsController(window, window.decorView)
+ .isAppearanceLightStatusBars = true
setContentView(binding?.root)
setSupportActionBar(binding?.toolbar?.toolbar)
@@ -93,10 +100,11 @@ class QuizResultActivity : AppCompatActivity() {
}
/**
- * Function to call intent to an activity
- * @param context
- * @param cls
- * @param flags
+ * Starts an activity using the provided context, target class, and intent flags.
+ *
+ * @param context The context used to start the activity.
+ * @param cls The target activity class.
+ * @param flags A variable number of intent flags to apply to the Intent.
*/
companion object {
fun startActivityWithFlags(context: Context, cls: Class