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/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 8a4d530c4..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
@@ -74,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
@@ -102,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
@@ -116,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
@@ -125,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
@@ -314,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
@@ -544,6 +541,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
)
binding.progressBarEdit.visibility = View.GONE
+ binding.descriptionEdit.visibility = View.VISIBLE
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -811,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
+ }
}
}
@@ -907,7 +922,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
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))
}
@@ -915,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()
}
@@ -1012,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
)
}
@@ -1646,7 +1661,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
getString(R.string.cancel),
{
val reason: String = input.text.toString()
- onDeleteClickeddialogtext(reason)
+ onDeleteClickedDialogText(reason)
},
{},
input
@@ -1700,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,
@@ -1736,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))
}
}
@@ -2107,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,
@@ -2136,11 +2153,8 @@ fun FileUsagesContainer(
MediaDetailViewModel.FileUsagesContainerState.Loading -> {
LinearProgressIndicator()
}
-
is MediaDetailViewModel.FileUsagesContainerState.Success -> {
-
val data = commonsContainerState.data
-
if (data.isNullOrEmpty()) {
ListItem(headlineContent = {
Text(
@@ -2160,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(
@@ -2168,11 +2182,11 @@ fun FileUsagesContainer(
textDecoration = TextDecoration.Underline
)
)
- })
+ }
+ )
}
}
}
-
is MediaDetailViewModel.FileUsagesContainerState.Error -> {
ListItem(headlineContent = {
Text(
@@ -2182,12 +2196,10 @@ fun FileUsagesContainer(
)
})
}
-
MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
}
}
-
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -2198,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,
@@ -2215,11 +2224,8 @@ fun FileUsagesContainer(
MediaDetailViewModel.FileUsagesContainerState.Loading -> {
LinearProgressIndicator()
}
-
is MediaDetailViewModel.FileUsagesContainerState.Success -> {
-
val data = globalContainerState.data
-
if (data.isNullOrEmpty()) {
ListItem(headlineContent = {
Text(
@@ -2238,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(
@@ -2257,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 643374e54..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
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 a1bad1f26..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
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 7445a6526..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
}
@@ -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, vararg flags: Int) {
@@ -185,7 +193,7 @@ class QuizResultActivity : AppCompatActivity() {
alertadd.setPositiveButton(R.string.about_translate_proceed) { dialog, _ ->
shareScreen(screenshot)
}
- alertadd.setNegativeButton(android.R.string.cancel) { dialog, _ ->
+ alertadd.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.cancel()
}
alertadd.show()
diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt
index facc4384f..80128ba73 100644
--- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt
+++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt
@@ -3,17 +3,13 @@ package fr.free.nrw.commons.recentlanguages
import android.content.ContentValues
import android.database.Cursor
-import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
-import android.text.TextUtils
import fr.free.nrw.commons.BuildConfig
-import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME
-import javax.inject.Inject
-import timber.log.Timber
+import androidx.core.net.toUri
/**
@@ -23,27 +19,17 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
companion object {
private const val BASE_PATH = "recent_languages"
- val BASE_URI: Uri =
- Uri.parse(
- "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH"
- )
+ val BASE_URI: Uri = "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH".toUri()
/**
* Append language code to the base URI
* @param languageCode Code of a language
*/
@JvmStatic
- fun uriForCode(languageCode: String): Uri {
- return Uri.parse("$BASE_URI/$languageCode")
- }
+ fun uriForCode(languageCode: String): Uri = "$BASE_URI/$languageCode".toUri()
}
- @Inject
- lateinit var dbOpenHelper: DBOpenHelper
-
- override fun getType(uri: Uri): String? {
- return null
- }
+ override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the recently used languages
@@ -60,11 +46,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
selectionArgs: Array?,
sortOrder: String?
): Cursor? {
- val queryBuilder = SQLiteQueryBuilder()
- queryBuilder.tables = TABLE_NAME
- val db = dbOpenHelper.readableDatabase
+ val queryBuilder = SQLiteQueryBuilder().apply {
+ tables = TABLE_NAME
+ }
+
val cursor = queryBuilder.query(
- db,
+ requireDb(),
projection,
selection,
selectionArgs,
@@ -89,12 +76,11 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
selection: String?,
selectionArgs: Array?
): Int {
- val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid URI: $uri")
- rowsUpdated = sqlDB.update(
+ rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_NAME = ?",
@@ -114,14 +100,13 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
* @param contentValues : new values to be entered to the database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
- val sqlDB = dbOpenHelper.writableDatabase
- val id = sqlDB.insert(
+ val id = requireDb().insert(
TABLE_NAME,
null,
contentValues
)
context?.contentResolver?.notifyChange(uri, null)
- return Uri.parse("$BASE_URI/$id")
+ return "$BASE_URI/$id".toUri()
}
/**
@@ -129,9 +114,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
* @param uri : contains the URI for recently used languages
*/
override fun delete(uri: Uri, s: String?, strings: Array?): Int {
- val db = dbOpenHelper.readableDatabase
- Timber.d("Deleting recently used language %s", uri.lastPathSegment)
- val rows = db.delete(
+ val rows = requireDb().delete(
TABLE_NAME,
"language_code = ?",
arrayOf(uri.lastPathSegment)
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt
index 443a112dd..19f526906 100644
--- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt
@@ -199,10 +199,11 @@ class UploadRepository @Inject constructor(
}
/**
- * Query the RemoteDataSource for image duplicity check
+ * Queries the RemoteDataSource to check if the image is a duplicate.
*
- * @param filePath file to be checked
- * @return IMAGE_DUPLICATE or IMAGE_OK
+ * @param originalFilePath The original file to be checked.
+ * @param modifiedFilePath The modified version of the file (if any).
+ * @return IMAGE_DUPLICATE if the image already exists, otherwise IMAGE_OK.
*/
fun checkDuplicateImage(originalFilePath: Uri?, modifiedFilePath: Uri?): Single {
return uploadModel.checkDuplicateImage(originalFilePath, modifiedFilePath)
diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt
index 01a1005fe..dccb77af1 100644
--- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt
@@ -16,6 +16,7 @@ import fr.free.nrw.commons.databinding.ActivityReviewBinding
import fr.free.nrw.commons.delete.DeleteHelper
import fr.free.nrw.commons.media.MediaDetailFragment
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.ViewUtil
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -55,10 +56,10 @@ class ReviewActivity : BaseActivity() {
}
/**
- * Consumers should be simply using this method to use this activity.
+ * Starts the ReviewActivity.
*
- * @param context
- * @param title Page title
+ * @param context The context used to start the activity.
+ * @param title The page title (currently unused).
*/
companion object {
fun startYourself(context: Context, title: String) {
@@ -73,6 +74,7 @@ class ReviewActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReviewBinding.inflate(layoutInflater)
+ applyEdgeToEdgeAllInsets(binding.root)
setContentView(binding.root)
setSupportActionBar(binding.toolbarBinding?.toolbar)
@@ -107,10 +109,8 @@ class ReviewActivity : BaseActivity() {
setUpMediaDetailFragment()
}
- binding.skipImage.setOnTouchListener { _, event ->
- if (event.action == MotionEvent.ACTION_UP &&
- event.rawX >= (binding.skipImage.right - binding.skipImage.compoundDrawables[2].bounds.width())
- ) {
+ binding.skipImageInfo?.setOnTouchListener { _, event ->
+ if (event.action == MotionEvent.ACTION_UP) {
showSkipImageInfo()
true
} else {
@@ -238,7 +238,7 @@ class ReviewActivity : BaseActivity() {
this,
getString(R.string.skip_image).uppercase(Locale.ROOT),
getString(R.string.skip_image_explanation),
- getString(android.R.string.ok),
+ getString(R.string.ok),
null,
null,
null
@@ -250,7 +250,7 @@ class ReviewActivity : BaseActivity() {
this,
getString(R.string.title_activity_review),
getString(R.string.review_image_explanation),
- getString(android.R.string.ok),
+ getString(R.string.ok),
null,
null,
null
diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt
index 91c88d7b0..233e688f4 100644
--- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt
@@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.MenuItem
import fr.free.nrw.commons.databinding.ActivitySettingsBinding
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
/**
@@ -21,6 +22,7 @@ class SettingsActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
val view = binding.root
+ applyEdgeToEdgeAllInsets(view)
setContentView(view)
setSupportActionBar(binding.toolbarBinding.toolbar)
diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt
index 161927d03..c38ed1ecb 100644
--- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt
@@ -1,6 +1,7 @@
package fr.free.nrw.commons.settings
import android.Manifest.permission
+import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Context.MODE_PRIVATE
@@ -33,9 +34,7 @@ import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
-import fr.free.nrw.commons.BuildConfig.MOBILE_META_URL
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.activity.SingleWebViewActivity
import fr.free.nrw.commons.campaigns.CampaignView
import fr.free.nrw.commons.contributions.ContributionController
@@ -53,6 +52,7 @@ import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.PermissionUtils
import fr.free.nrw.commons.utils.StringUtil
import fr.free.nrw.commons.utils.ViewUtil
+import fr.free.nrw.commons.utils.handleWebUrl
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
@@ -239,7 +239,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
val betaTesterPreference: Preference? = findPreference("becomeBetaTester")
betaTesterPreference?.setOnPreferenceClickListener {
- Utils.handleWebUrl(requireActivity(), Uri.parse(getString(R.string.beta_opt_in_link)))
+ handleWebUrl(
+ requireActivity(),
+ Uri.parse(getString(R.string.beta_opt_in_link))
+ )
true
}
@@ -276,9 +279,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
/**
- * Asks users to provide location access
- *
- * @param activity
+ * Asks users to provide location access.
*/
private fun createDialogsAndHandleLocationPermissions() {
inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
@@ -298,11 +299,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
getString(R.string.ok),
getString(R.string.read_help_link),
{ },
- { Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)) },
+ { handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)) },
null
)
}
+ // Remove the space for icons in the settings menu.
+ // This uses an internal API that shouldn't be used in app code,
+ // but it appears to be the most robust way to do this at the moment,
+ // disable the warning.
+ @SuppressLint("RestrictedApi")
override fun onCreateAdapter(preferenceScreen: PreferenceScreen): Adapter
{
return object : PreferenceGroupAdapter(preferenceScreen) {
diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt
index d2d936460..d317a7d35 100644
--- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt
@@ -4,6 +4,7 @@ import android.content.res.Configuration
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.WindowManager
+import androidx.activity.enableEdgeToEdge
import javax.inject.Inject
import javax.inject.Named
import fr.free.nrw.commons.R
@@ -36,6 +37,7 @@ abstract class BaseActivity : CommonsDaggerAppCompatActivity() {
1f
)
adjustFontScale(resources.configuration, fontScale)
+ enableEdgeToEdge()
}
override fun onResume() {
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Description.kt b/app/src/main/java/fr/free/nrw/commons/upload/Description.kt
index 83902aae4..cb2a85362 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/Description.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.kt
@@ -6,29 +6,29 @@ package fr.free.nrw.commons.upload
class Description {
/**
* The language code, e.g., "en" or "fr".
- * @param languageCode The language code.
+ * @property languageCode The language code.
*/
var languageCode: String? = null
/**
* The description text for the item being uploaded.
- * @param descriptionText The description text.
+ * @property descriptionText The description text.
*/
var descriptionText: String? = null
/**
* The index of the language selected in a spinner with [SpinnerLanguagesAdapter].
- * @param selectedLanguageIndex The index of the selected language.
+ * @property selectedLanguageIndex The index of the selected language.
*/
var selectedLanguageIndex = -1
/**
* Indicates if the description was added manually (by the user or programmatically).
- * @param manuallyAdded Sets to true if the description was manually added by the user.
- * @return True if the description was manually added.
*/
var isManuallyAdded = false
-
+ /**
+ * Returns true if the description text is null or empty.
+ */
val isEmpty: Boolean
get() = descriptionText == null || descriptionText!!.isEmpty()
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt
index 617da88a0..ed1867472 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt
@@ -137,13 +137,12 @@ class FileProcessor
}
}
- /**
- * Find other images around the same location that were taken within the last 20 sec
- *
- * @param originalImageCoordinates
- * @param fileBeingProcessed
- * @param similarImageInterface
- */
+ /**
+ * Finds other images around the same location that were taken within a ±120 sec window.
+ *
+ * @param fileBeingProcessed The file currently being checked.
+ * @param similarImageInterface Callback to display similar images if any are found.
+ */
private fun findOtherImages(
fileBeingProcessed: File,
similarImageInterface: SimilarImageInterface?,
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt
index 3acd13c65..cbec7559f 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt
@@ -138,10 +138,10 @@ class ImageProcessingService @Inject constructor(
}
/**
- * Checks for duplicate image
+ * Checks for duplicate image by calculating its SHA1 hash and querying the media client.
*
- * @param filePath file to be checked
- * @return IMAGE_DUPLICATE or IMAGE_OK
+ * @param inputStream The input stream of the file to check.
+ * @return IMAGE_DUPLICATE if the file exists, or IMAGE_OK otherwise.
*/
private fun checkDuplicateImage(inputStream: InputStream): Single {
return Single.fromCallable { fileUtilsWrapper.getSHA1(inputStream) }
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt
index 0c4ded8b2..2de17f849 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt
@@ -1,11 +1,11 @@
package fr.free.nrw.commons.upload
import android.content.Context
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.filepicker.UploadableFile.DateTimeWithSource
import fr.free.nrw.commons.settings.Prefs.Licenses
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
+import fr.free.nrw.commons.utils.getWikiLovesMonumentsYear
import org.apache.commons.lang3.StringUtils
import java.text.SimpleDateFormat
import java.util.Calendar
@@ -49,7 +49,7 @@ class PageContentsCreator @Inject constructor(private val context: Context) {
String.format(
Locale.ENGLISH,
"{{Wiki Loves Monuments %d|1= %s}}\n",
- Utils.getWikiLovesMonumentsYear(Calendar.getInstance()),
+ getWikiLovesMonumentsYear(Calendar.getInstance()),
contribution.countryCode
)
)
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
index ee0b21210..66e0257f6 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt
@@ -13,6 +13,7 @@ import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.provider.Settings
import android.view.View
+import android.view.inputmethod.InputMethodManager
import android.widget.CheckBox
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
@@ -38,6 +39,7 @@ import fr.free.nrw.commons.mwapi.UserClient
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.upload.ThumbnailsAdapter.OnThumbnailDeletedListener
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment
import fr.free.nrw.commons.upload.depicts.DepictsFragment
@@ -173,7 +175,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ // Ensure basicKvStoreFactory is always initialized before use
+ presenter?.setupBasicKvStoreFactory { BasicKvStore(this@UploadActivity, it) }
+
_binding = ActivityUploadBinding.inflate(layoutInflater)
+ applyEdgeToEdgeAllInsets(_binding!!.root, false)
setContentView(binding.root)
// Overrides the back button to make sure the user is prepared to lose their progress
@@ -440,7 +446,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
this,
getString(R.string.storage_permissions_denied),
getString(R.string.unable_to_share_upload_item),
- getString(android.R.string.ok)
+ getString(R.string.ok)
) { finish() }
} else {
showAlertDialog(
@@ -449,7 +455,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
getString(
R.string.write_storage_permission_rationale_for_image_share
),
- getString(android.R.string.ok)
+ getString(R.string.ok)
) { checkStoragePermissions() }
}
}
@@ -502,24 +508,17 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
fragments = mutableListOf()
}
-
for (uploadableFile in uploadableFiles) {
val uploadMediaDetailFragment = UploadMediaDetailFragment()
- if (!uploadIsOfAPlace) {
+ // set fragment properties but defer initialization
+ uploadMediaDetailFragment.uploadableFile = uploadableFile
+ uploadMediaDetailFragment.place = place
+ uploadMediaDetailFragment.inAppPictureLocation = if (!uploadIsOfAPlace) {
handleLocation()
- uploadMediaDetailFragment.setImageToBeUploaded(
- uploadableFile,
- place,
- currLocation
- )
- locationManager!!.unregisterLocationManager()
+ currLocation
} else {
- uploadMediaDetailFragment.setImageToBeUploaded(
- uploadableFile,
- place,
- currLocation
- )
+ currLocation
}
val uploadMediaDetailFragmentCallback: UploadMediaDetailFragmentCallback =
@@ -574,13 +573,19 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
if (isFragmentsSaved) {
val fragment = fragments!![0] as UploadMediaDetailFragment?
fragment!!.fragmentCallback = uploadMediaDetailFragmentCallback
+ fragment.initializeFragment()
} else {
uploadMediaDetailFragment.fragmentCallback = uploadMediaDetailFragmentCallback
fragments!!.add(uploadMediaDetailFragment)
}
}
- //If fragments are not created, create them and add them to the fragments ArrayList
+ // unregister location manager after loop if needed
+ if (!uploadIsOfAPlace) {
+ locationManager!!.unregisterLocationManager()
+ }
+
+ // If fragments are not created, create them and add them to the fragments ArrayList
if (!isFragmentsSaved) {
uploadCategoriesFragment = UploadCategoriesFragment()
if (place != null) {
@@ -800,6 +805,19 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onNextButtonClicked(index: Int) {
if (index < fragments!!.size - 1) {
+ // Hide the keyboard before navigating to Media License screen
+ val isUploadCategoriesFragment = fragments!!.getOrNull(index)?.let {
+ it is UploadCategoriesFragment
+ } ?: false
+ if (isUploadCategoriesFragment) {
+ val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ currentFocus?.let { focusedView ->
+ inputMethodManager.hideSoftInputFromWindow(
+ focusedView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ }
+ }
binding.vpUpload.setCurrentItem(index + 1, false)
fragments!![index + 1].onBecameVisible()
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
@@ -903,7 +921,6 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
// Save the user's choice to not show the dialog again
defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true)
}
- presenter!!.setupBasicKvStoreFactory { BasicKvStore(this@UploadActivity, it) }
presenter!!.checkImageQuality(0)
UploadMediaPresenter.isCategoriesDialogShowing = false
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
index f357cd112..6d2321def 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
@@ -1,10 +1,10 @@
package fr.free.nrw.commons.upload
import android.net.Uri
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper.Companion.getExtensionFromMimeType
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.utils.ImageUtils
+import fr.free.nrw.commons.utils.fixExtension
class UploadItem(
var mediaUri: Uri?,
@@ -32,7 +32,7 @@ class UploadItem(
* languages have been entered, the first language is used.
*/
val filename: String
- get() = Utils.fixExtension(
+ get() = fixExtension(
uploadMediaDetails[0].captionText,
getExtensionFromMimeType(mimeType)
)
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt
index 351e53124..446d1f0cf 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt
@@ -10,18 +10,15 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class UploadMediaDetail(
/**
- * The language code ie. "en" or "fr".
- * @param languageCode The language code ie. "en" or "fr".
+ * The language code, e.g., "en" or "fr".
*/
var languageCode: String? = null,
/**
* The description text for the item being uploaded.
- * @param descriptionText The description text.
*/
var descriptionText: String? = "",
/**
* The caption text for the item being uploaded.
- * @param captionText The caption text.
*/
var captionText: String = "",
) : Parcelable {
@@ -35,15 +32,11 @@ data class UploadMediaDetail(
/**
* The index of the language selected in a spinner with [SpinnerLanguagesAdapter].
- * @return The index of the selected language.
- * @param selectedLanguageIndex The index of the language selected.
*/
var selectedLanguageIndex: Int = -1
/**
- * Returns if the description was added manually (by the user, or programmatically).
- * @return True if the description was manually added.
- * @param manuallyAdded Sets to true if the description was manually added.
+ * Indicates whether the description was added manually (by the user or programmatically).
*/
var isManuallyAdded: Boolean = false
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt
index d4baf21c8..1d5e59468 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt
@@ -30,9 +30,10 @@ class UploadMediaDetailInputFilter : InputFilter {
patterns.any { it.matcher(source).find() }
/**
- * Removes any blocklisted characters from the source text.
- * @param source input text
- * @return a cleaned character sequence
+ * Removes any blocklisted characters from the input text.
+ *
+ * @param input The input text to be cleaned.
+ * @return A cleaned character sequence with blocklisted patterns removed.
*/
private fun removeBlocklisted(input: CharSequence): CharSequence {
var source = input
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
index 2cda7a890..954079a45 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.kt
@@ -98,10 +98,11 @@ class UploadModel @Inject internal constructor(
imageProcessingService.validateImage(uploadItem, inAppPictureLocation)
/**
- * Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate
+ * Calls checkDuplicateImage() of ImageProcessingService to check if the image is a duplicate.
*
- * @param filePath file to be checked
- * @return IMAGE_DUPLICATE or IMAGE_OK
+ * @param originalFilePath The original file URI.
+ * @param modifiedFilePath The modified file URI.
+ * @return IMAGE_DUPLICATE if the file already exists, IMAGE_OK otherwise.
*/
fun checkDuplicateImage(originalFilePath: Uri?, modifiedFilePath: Uri?): Single =
imageProcessingService.checkIfFileAlreadyExists(
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
index 5d721f408..251f868ad 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
@@ -34,8 +34,7 @@ class UploadPresenter @Inject internal constructor(
private val compositeDisposable = CompositeDisposable()
- lateinit var basicKvStoreFactory: (String) -> BasicKvStore
-
+ private var basicKvStoreFactory: ((String) -> BasicKvStore)? = null
/**
* Called by the submit button in [UploadActivity]
*/
@@ -132,14 +131,38 @@ class UploadPresenter @Inject internal constructor(
basicKvStoreFactory = factory
}
+ /**
+ * Returns the current BasicKvStore factory or throws if not initialized.
+ *
+ * @throws IllegalStateException if basicKvStoreFactory has not been initialized.
+ */
+ private fun getBasicKvStoreFactory(): (String) -> BasicKvStore {
+ return basicKvStoreFactory ?: throw IllegalStateException("basicKvStoreFactory has not been initialized")
+ }
+
+ /**
+ * Ensures that the BasicKvStore factory has been initialized before use.
+ *
+ * @throws IllegalStateException if the factory is null.
+ */
+ private fun requireFactoryInitialized() {
+ val field = this::class.java.getDeclaredField("basicKvStoreFactory")
+ field.isAccessible = true
+ val value = field.get(this)
+ if (value == null) {
+ throw IllegalStateException("basicKvStoreFactory must be initialized before use. Please call setupBasicKvStoreFactory() before using presenter methods that require it.")
+ }
+ }
+
/**
* Calls checkImageQuality of UploadMediaPresenter to check image quality of next image
*
* @param uploadItemIndex Index of next image, whose quality is to be checked
*/
override fun checkImageQuality(uploadItemIndex: Int) {
+ requireFactoryInitialized()
repository.getUploadItem(uploadItemIndex)?.let {
- presenter.setupBasicKvStoreFactory(basicKvStoreFactory)
+ presenter.setupBasicKvStoreFactory(getBasicKvStoreFactory())
presenter.checkImageQuality(it, uploadItemIndex)
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt
index aeaefa302..665f106e2 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt
@@ -10,6 +10,7 @@ import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.databinding.ActivityUploadProgressBinding
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import javax.inject.Inject
/**
@@ -28,8 +29,6 @@ class UploadProgressActivity : BaseActivity() {
@Inject
lateinit var contributionDao: ContributionDao
- val fragmentList: MutableList = ArrayList()
- val titleList: MutableList = ArrayList()
var isPaused = true
var isPendingIconsVisible = true
var isErrorIconsVisisble = false
@@ -37,8 +36,9 @@ class UploadProgressActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUploadProgressBinding.inflate(layoutInflater)
+ applyEdgeToEdgeAllInsets(binding.root)
setContentView(binding.root)
- viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
+ viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
binding.uploadProgressViewPager.setAdapter(viewPagerAdapter)
binding.uploadProgressViewPager.setId(R.id.upload_progress_view_pager)
binding.uploadProgressTabLayout.setupWithViewPager(binding.uploadProgressViewPager)
@@ -58,11 +58,7 @@ class UploadProgressActivity : BaseActivity() {
override fun onPageSelected(position: Int) {
updateMenuItems(position)
- if (position == 2) {
- binding.uploadProgressViewPager.setCanScroll(false)
- } else {
- binding.uploadProgressViewPager.setCanScroll(true)
- }
+ binding.uploadProgressViewPager.canScroll = (position != 2)
}
override fun onPageScrollStateChanged(state: Int) {
@@ -81,11 +77,10 @@ class UploadProgressActivity : BaseActivity() {
pendingUploadsFragment = PendingUploadsFragment()
failedUploadsFragment = FailedUploadsFragment()
- fragmentList.add(pendingUploadsFragment!!)
- titleList.add(getString(R.string.pending))
- fragmentList.add(failedUploadsFragment!!)
- titleList.add(getString(R.string.failed))
- viewPagerAdapter!!.setTabData(fragmentList, titleList)
+ viewPagerAdapter!!.setTabs(
+ R.string.pending to pendingUploadsFragment!!,
+ R.string.failed to failedUploadsFragment!!
+ )
viewPagerAdapter!!.notifyDataSetChanged()
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt
index 183c7cd93..29e5ba90b 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt
@@ -17,6 +17,11 @@ interface CategoriesContract {
fun showError(stringResourceId: Int)
+ /**
+ * Show a cancelable AlertDialog with a given message.
+ */
+ fun showErrorDialog(message: String)
+
fun setCategories(categories: List?)
fun goToNextScreen()
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt
index dbeeae6ff..a1a96f2ac 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt
@@ -12,6 +12,7 @@ import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.proxy
+import fr.free.nrw.commons.wikidata.mwapi.MwIOException
import io.reactivex.Observable
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -75,7 +76,12 @@ class CategoriesPresenter
},
{ t: Throwable? ->
view.showProgress(false)
- view.showError(R.string.no_categories_found)
+ view.showError(R.string.error_loading_categories)
+ val mwException = t as? MwIOException
+ view.showErrorDialog(
+ if (mwException == null) ""
+ else "\n${mwException.error.title} / ${mwException.error.details}"
+ )
Timber.e(t)
},
),
@@ -194,7 +200,12 @@ class CategoriesPresenter
},
{ t: Throwable? ->
view.showProgress(false)
- view.showError(R.string.no_categories_found)
+ view.showError(R.string.error_loading_categories)
+ val mwException = t as? MwIOException
+ view.showErrorDialog(
+ if (mwException == null) ""
+ else "\n${mwException.error.title} / ${mwException.error.details}"
+ )
Timber.e(t)
},
),
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt
index 262013045..ef4521431 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt
@@ -10,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.jakewharton.rxbinding2.view.RxView
@@ -26,12 +27,12 @@ import fr.free.nrw.commons.media.MediaDetailFragment
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadBaseFragment
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import fr.free.nrw.commons.utils.handleKeyboardInsets
import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY
import io.reactivex.Notification
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import timber.log.Timber
-import java.util.Objects
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -69,6 +70,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
savedInstanceState: Bundle?
): View? {
binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false)
+ binding!!.llContainerButtons.handleKeyboardInsets()
return binding!!.root
}
@@ -115,7 +117,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
requireActivity(),
getString(R.string.categories_activity_title),
getString(R.string.categories_tooltip),
- getString(android.R.string.ok),
+ getString(R.string.ok),
null
)
}
@@ -197,6 +199,15 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
binding?.tilContainerSearch?.error = getString(stringResourceId)
}
+ override fun showErrorDialog(message: String) {
+ AlertDialog
+ .Builder(requireContext())
+ .setMessage(getString(R.string.error_loading_categories) + "\n" + message)
+ .setCancelable(false)
+ .setNegativeButton(R.string.ok){_,_ -> }
+ .show()
+ }
+
override fun setCategories(categories: List?) {
if (adapter == null) {
Timber.e("Adapter is null in setCategories")
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt
index 39bcabb46..5dcc2bf86 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt
@@ -27,6 +27,7 @@ import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadBaseFragment
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import fr.free.nrw.commons.utils.handleKeyboardInsets
import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE
import io.reactivex.Notification
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -69,6 +70,7 @@ class DepictsFragment : UploadBaseFragment(), DepictsContract.View {
savedInstanceState: Bundle?
): View {
_binding = UploadDepictsFragmentBinding.inflate(inflater, container, false)
+ _binding!!.navigationButtonsContainer.handleKeyboardInsets()
return binding.root
}
@@ -114,7 +116,7 @@ class DepictsFragment : UploadBaseFragment(), DepictsContract.View {
requireActivity(),
getString(R.string.depicts_step_title),
getString(R.string.depicts_tooltip),
- getString(android.R.string.ok),
+ getString(R.string.ok),
null
)
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt
index 0415d3270..a789ef362 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt
@@ -16,11 +16,13 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.databinding.FragmentMediaLicenseBinding
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadBaseFragment
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import fr.free.nrw.commons.utils.handleWebUrl
+import fr.free.nrw.commons.utils.toLicenseName
+import fr.free.nrw.commons.utils.toLicenseUrl
import timber.log.Timber
import javax.inject.Inject
@@ -68,7 +70,7 @@ class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View {
requireActivity(),
getString(R.string.license_step_title),
getString(R.string.license_tooltip),
- getString(android.R.string.ok),
+ getString(R.string.ok),
null
)
}
@@ -126,20 +128,20 @@ class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View {
}
override fun setSelectedLicense(license: String?) {
- var position = licenses!!.indexOf(getString(Utils.licenseNameFor(license)))
+ var position = license?.let { licenses!!.indexOf(getString(it.toLicenseName())) } ?: -1
// Check if position is valid
if (position < 0) {
Timber.d("Invalid position: %d. Using default licenses", position)
position = licenses!!.size - 1
- } else {
- Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)))
}
binding.spinnerLicenseList.setSelection(position)
}
override fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int) {
- val licenseHyperLink = "" +
- getString(Utils.licenseNameFor(selectedLicense)) + " "
+ if (selectedLicense == null) return
+
+ val licenseHyperLink = "" +
+ getString(selectedLicense.toLicenseName()) + " "
setTextViewHTML(
binding.tvShareLicenseSummary, resources
@@ -184,7 +186,7 @@ class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View {
}
private fun launchBrowser(hyperLink: String) =
- Utils.handleWebUrl(context, Uri.parse(hyperLink))
+ handleWebUrl(requireContext(), Uri.parse(hyperLink))
override fun onDestroyView() {
presenter.onDetachView()
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt
index 25d1a2324..df75019b2 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt
@@ -1,9 +1,9 @@
package fr.free.nrw.commons.upload.license
-import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.settings.Prefs
+import fr.free.nrw.commons.utils.toLicenseName
import timber.log.Timber
import java.lang.reflect.Method
import java.lang.reflect.Proxy
@@ -34,12 +34,14 @@ class MediaLicensePresenter @Inject constructor(
val licenses = repository.getLicenses()
view.setLicenses(licenses)
- var selectedLicense = defaultKVStore.getString(
+ //CC_BY_SA_4 is the default one used by the commons web app
+ var selectedLicense: String = defaultKVStore.getString(
Prefs.DEFAULT_LICENSE,
Prefs.Licenses.CC_BY_SA_4
- ) //CC_BY_SA_4 is the default one used by the commons web app
+ ) ?: Prefs.Licenses.CC_BY_SA_4
+
try { //I have to make sure that the stored default license was not one of the deprecated one's
- Utils.licenseNameFor(selectedLicense)
+ selectedLicense.toLicenseName()
} catch (exception: IllegalStateException) {
Timber.e(exception)
selectedLicense = Prefs.Licenses.CC_BY_SA_4
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt
index 4a4c13ba7..b3b067948 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt
@@ -50,6 +50,7 @@ import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
+import fr.free.nrw.commons.utils.handleKeyboardInsets
import timber.log.Timber
import java.io.File
import java.util.ArrayList
@@ -118,8 +119,8 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
private var basicKvStore: BasicKvStore? = null
private val keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing"
- private var uploadableFile: UploadableFile? = null
- private var place: Place? = null
+ internal var uploadableFile: UploadableFile? = null
+ internal var place: Place? = null
private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter
var indexOfFragment = 0
var isExpanded = true
@@ -141,18 +142,24 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
}
}
- fun setImageToBeUploaded(
- uploadableFile: UploadableFile?, place: Place?, inAppPictureLocation: LatLng?
- ) {
- this.uploadableFile = uploadableFile
- this.place = place
- this.inAppPictureLocation = inAppPictureLocation
- }
-
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false)
+ _binding!!.mediaDetailCardView.handleKeyboardInsets()
+ // intialise the adapter early to prevent uninitialized access
+ uploadMediaDetailAdapter = UploadMediaDetailAdapter(
+ this,
+ defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "")!!,
+ recentLanguagesDao, voiceInputResultLauncher
+ )
+ uploadMediaDetailAdapter.callback =
+ UploadMediaDetailAdapter.Callback { titleStringID: Int, messageStringId: Int ->
+ showInfoAlert(titleStringID, messageStringId)
+ }
+ uploadMediaDetailAdapter.eventListener = this
+ binding.rvDescriptions.layoutManager = LinearLayoutManager(context)
+ binding.rvDescriptions.adapter = uploadMediaDetailAdapter
return binding.root
}
@@ -161,20 +168,48 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
basicKvStore = BasicKvStore(requireActivity(), "CurrentUploadImageQualities")
- if (fragmentCallback != null) {
- indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this)
- initializeFragment()
- }
-
+ // restore adapter items from savedInstanceState if available
if (savedInstanceState != null) {
- if (uploadMediaDetailAdapter.items.isEmpty() && fragmentCallback != null) {
- uploadMediaDetailAdapter.items = savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)!!
- presenter.setUploadMediaDetails(uploadMediaDetailAdapter.items, indexOfFragment)
+ val savedItems = savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)
+ Timber.d("Restoring state: savedItems size = %s", savedItems?.size ?: "null")
+ if (savedItems != null && savedItems.isNotEmpty()) {
+ uploadMediaDetailAdapter.items = savedItems
+ // only call setUploadMediaDetails if indexOfFragment is valid
+ if (fragmentCallback != null) {
+ indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this)
+ if (indexOfFragment >= 0) {
+ presenter.setUploadMediaDetails(uploadMediaDetailAdapter.items, indexOfFragment)
+ Timber.d("Restored and set upload media details for index %d", indexOfFragment)
+ } else {
+ Timber.w("Invalid indexOfFragment %d, skipping setUploadMediaDetails", indexOfFragment)
+ }
+ } else {
+ Timber.w("fragmentCallback is null, skipping setUploadMediaDetails")
+ }
+ } else {
+ // initialize with a default UploadMediaDetail if saved state is empty or null
+ uploadMediaDetailAdapter.items = mutableListOf(UploadMediaDetail())
+ Timber.d("Initialized default UploadMediaDetail due to empty or null savedItems")
+ }
+ } else {
+ // intitialise with a default UploadMediaDetail for fresh fragment
+ if (uploadMediaDetailAdapter.items.isEmpty()) {
+ uploadMediaDetailAdapter.items = mutableListOf(UploadMediaDetail())
+ Timber.d("Initialized default UploadMediaDetail for new fragment")
}
}
+ if (fragmentCallback != null) {
+ indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this)
+ Timber.d("Fragment callback present, indexOfFragment = %d", indexOfFragment)
+ initializeFragment()
+ } else {
+ Timber.w("Fragment callback is null, skipping initializeFragment")
+ }
+
try {
- if (!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) {
+ if (indexOfFragment >= 0 && !presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) {
+ Timber.d("Image quality check failed, redirecting to MainActivity")
startActivityWithFlags(
requireActivity(),
MainActivity::class.java,
@@ -182,11 +217,12 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
Intent.FLAG_ACTIVITY_SINGLE_TOP
)
}
- } catch (_: Exception) {
+ } catch (e: Exception) {
+ Timber.e(e, "Error during image quality check")
}
}
- private fun initializeFragment() {
+ internal fun initializeFragment() {
if (_binding == null) {
return
}
@@ -204,7 +240,6 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
presenter.setupBasicKvStoreFactory { BasicKvStore(requireActivity(), it) }
presenter.receiveImage(uploadableFile, place, inAppPictureLocation)
- initRecyclerView()
with (binding){
if (indexOfFragment == 0) {
@@ -263,30 +298,12 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
}
}
- /**
- * init the description recycler veiw and caption recyclerview
- */
- private fun initRecyclerView() {
- uploadMediaDetailAdapter = UploadMediaDetailAdapter(
- this,
- defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "")!!,
- recentLanguagesDao, voiceInputResultLauncher
- )
- uploadMediaDetailAdapter.callback =
- UploadMediaDetailAdapter.Callback { titleStringID: Int, messageStringId: Int ->
- showInfoAlert(titleStringID, messageStringId)
- }
- uploadMediaDetailAdapter.eventListener = this
- binding.rvDescriptions.layoutManager = LinearLayoutManager(context)
- binding.rvDescriptions.adapter = uploadMediaDetailAdapter
- }
-
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) {
showAlertDialog(
requireActivity(),
getString(titleStringID),
getString(messageStringId),
- getString(android.R.string.ok),
+ getString(R.string.ok),
null
)
}
@@ -588,16 +605,14 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
var defaultLongitude = -122.431297
var defaultZoom = 16.0
- val locationPickerIntent: Intent
-
/* Retrieve image location from EXIF if present or
check if user has provided location while using the in-app camera.
Use location of last UploadItem if none of them is available */
+ val locationPickerIntent: Intent
if (uploadItem.gpsCoords != null && uploadItem.gpsCoords!!
.decLatitude != 0.0 && uploadItem.gpsCoords!!.decLongitude != 0.0
) {
- defaultLatitude = uploadItem.gpsCoords!!
- .decLatitude
+ defaultLatitude = uploadItem.gpsCoords!!.decLatitude
defaultLongitude = uploadItem.gpsCoords!!.decLongitude
defaultZoom = uploadItem.gpsCoords!!.zoomLevel
@@ -613,8 +628,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
defaultLongitude = locationLatLng[1].toDouble()
}
if (defaultKvStore.getString(LAST_ZOOM) != null) {
- defaultZoom = defaultKvStore.getString(LAST_ZOOM)!!
- .toDouble()
+ defaultZoom = defaultKvStore.getString(LAST_ZOOM)!!.toDouble()
}
locationPickerIntent = LocationPicker.IntentBuilder()
@@ -821,6 +835,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
{
showProgress(false)
uploadItem.imageQuality = IMAGE_OK
+ uploadItem.hasInvalidLocation = false // Reset invalid location flag when user confirms upload
},
{
presenterCallback!!.deletePictureAtIndex(index)
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt
index c368b96ac..d6d774208 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt
@@ -54,7 +54,7 @@ interface UploadMediaDetailsContract {
fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem)
}
- interface UserActionListener : BasePresenter {
+ interface UserActionListener : BasePresenter {
fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore)
fun receiveImage(
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt
index 77999cf2f..6e26e02a6 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt
@@ -69,7 +69,18 @@ class UploadMediaPresenter @Inject constructor(
uploadMediaDetails: List,
uploadItemIndex: Int
) {
- repository.getUploads()[uploadItemIndex].uploadMediaDetails = uploadMediaDetails.toMutableList()
+ val uploadItems = repository.getUploads()
+ if (uploadItemIndex >= 0 && uploadItemIndex < uploadItems.size) {
+ if (uploadMediaDetails.isNotEmpty()) {
+ uploadItems[uploadItemIndex].uploadMediaDetails = uploadMediaDetails.toMutableList()
+ Timber.d("Set uploadMediaDetails for index %d, size %d", uploadItemIndex, uploadMediaDetails.size)
+ } else {
+ uploadItems[uploadItemIndex].uploadMediaDetails = mutableListOf(UploadMediaDetail())
+ Timber.w("Received empty uploadMediaDetails for index %d, initialized default", uploadItemIndex)
+ }
+ } else {
+ Timber.e("Invalid index %d for uploadItems size %d, skipping setUploadMediaDetails", uploadItemIndex, uploadItems.size)
+ }
}
override fun setupBasicKvStoreFactory(factory: (String) -> BasicKvStore) {
@@ -107,7 +118,10 @@ class UploadMediaPresenter @Inject constructor(
view.showProgress(false)
val gpsCoords = uploadItem.gpsCoords
val hasImageCoordinates = gpsCoords != null && gpsCoords.imageCoordsExists
- if (hasImageCoordinates && place == null) {
+
+ // Only check for nearby places if image has coordinates AND no place was pre-selected
+ // This prevents the popup from appearing when uploading from Nearby feature
+ if (hasImageCoordinates && place == null && uploadItem.place == null) {
checkNearbyPlaces(uploadItem)
}
}, { throwable: Throwable? ->
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt
index 6d28085b2..21db20f1b 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt
@@ -393,6 +393,12 @@ class UploadWorker(
makeWikiDataEdit(uploadResult, contribution)
}
showSuccessNotification(contribution)
+ if (appContext.contentResolver.persistedUriPermissions.any {
+ it.uri == contribution.contentUri }) {
+ appContext.contentResolver.releasePersistableUriPermission(
+ contribution.contentUri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+ }
} else {
Timber.e("Stash Upload failed")
showFailedNotification(contribution)
@@ -472,7 +478,10 @@ class UploadWorker(
if (wikiDataPlace != null) {
if (!contribution.hasInvalidLocation()) {
var revisionID: Long? = null
+ val p18WasSkipped = !wikiDataPlace.imageValue.isNullOrBlank()
try {
+ if (!p18WasSkipped) {
+ // Only set P18 if the place does not already have a picture
revisionID =
wikidataEditService.createClaim(
wikiDataPlace,
@@ -489,9 +498,11 @@ class UploadWorker(
.subscribeOn(Schedulers.io())
.blockingAwait()
Timber.d("Updated WikiItem place ${place.name} with image ${place.pic}")
+ }
}
- showSuccessNotification(contribution)
}
+ // Always show success notification, whether P18 was set or skipped
+ showSuccessNotification(contribution)
} catch (exception: Exception) {
Timber.e(exception)
}
@@ -500,6 +511,7 @@ class UploadWorker(
wikidataEditService.handleImageClaimResult(
contribution.wikidataPlace!!,
revisionID,
+ p18WasSkipped = p18WasSkipped
)
}
} else {
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ClipboardUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ClipboardUtils.kt
new file mode 100644
index 000000000..64d3636f0
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ClipboardUtils.kt
@@ -0,0 +1,20 @@
+package fr.free.nrw.commons.utils
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Context.CLIPBOARD_SERVICE
+
+object ClipboardUtils {
+ // Convenience for Java usages - remove when they are converted.
+ @JvmStatic
+ fun copy(label: String?, text: String?, context: Context) {
+ context.copyToClipboard(label, text)
+ }
+}
+
+fun Context.copyToClipboard(label: String?, text: String?) {
+ with(getSystemService(CLIPBOARD_SERVICE) as ClipboardManager) {
+ setPrimaryClip(ClipData.newPlainText(label, text))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt
index 332c8d023..95fa62a20 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt
@@ -12,9 +12,9 @@ object ConfigUtils {
val isBetaFlavour: Boolean = BuildConfig.FLAVOR == "beta"
@JvmStatic
- private fun Context.getVersionName(): String =
+ private fun Context.getVersionName(): String? =
try {
- packageManager.getPackageInfo(packageName, 0).versionName
+ packageManager.getPackageInfo(packageName, 0).versionName ?: BuildConfig.VERSION_NAME
} catch (e: PackageManager.NameNotFoundException) {
BuildConfig.VERSION_NAME
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt
new file mode 100644
index 000000000..737f34614
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt
@@ -0,0 +1,51 @@
+package fr.free.nrw.commons.utils
+
+import android.annotation.SuppressLint
+import android.database.Cursor
+
+fun Cursor.getStringArray(name: String): List =
+ stringToArray(getString(name))
+
+/**
+ * Gets the String at the current row and specified column.
+ *
+ * @param name The name of the column to get the String from.
+ * @return The String if the column exists. Else, null is returned.
+ */
+@SuppressLint("Range")
+fun Cursor.getString(name: String): String? {
+ val index = getColumnIndex(name)
+ if (index == -1) {
+ return null
+ }
+ return getString(index)
+}
+
+@SuppressLint("Range")
+fun Cursor.getInt(name: String): Int =
+ getInt(getColumnIndex(name))
+
+@SuppressLint("Range")
+fun Cursor.getLong(name: String): Long =
+ getLong(getColumnIndex(name))
+
+/**
+ * Converts string to List
+ * @param listString comma separated single string from of list items
+ * @return List of string
+ */
+fun stringToArray(listString: String?): List {
+ if (listString.isNullOrEmpty()) return emptyList();
+ val elements = listString.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ return listOf(*elements)
+}
+
+/**
+ * Converts string to List
+ * @param list list of items
+ * @return string comma separated single string of items
+ */
+fun arrayToString(list: List?): String? {
+ return list?.joinToString(",")
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt
new file mode 100644
index 000000000..04007485d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/EdgeToEdgeUtils.kt
@@ -0,0 +1,212 @@
+package fr.free.nrw.commons.utils
+
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.marginBottom
+import androidx.core.view.marginLeft
+import androidx.core.view.marginRight
+import androidx.core.view.marginTop
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
+import fr.free.nrw.commons.R
+
+/**
+ * Applies edge-to-edge system bar insets to a [View]’s margins using a custom adjustment block.
+ *
+ * Stores the initial margins to ensure inset calculations are additive, and applies the provided
+ * [block] with an [InsetsAccumulator] containing initial and system bar inset values.
+ *
+ * @param typeMask The type of window insets to apply. Defaults to [WindowInsetsCompat.Type.systemBars].
+ * @param shouldConsumeInsets If `true`, the insets are consumed and not propagated to child views.
+ * @param block Lambda applied to update [MarginLayoutParams] using the accumulated insets.
+ */
+fun View.applyEdgeToEdgeInsets(
+ typeMask: Int = WindowInsetsCompat.Type.systemBars(),
+ shouldConsumeInsets: Boolean = true,
+ block: MarginLayoutParams.(InsetsAccumulator) -> Unit
+) {
+ ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
+ val insets = windowInsets.getInsets(typeMask)
+
+ val initialTop = if (view.getTag(R.id.initial_margin_top) != null) {
+ view.getTag(R.id.initial_margin_top) as Int
+ } else {
+ view.setTag(R.id.initial_margin_top, view.marginTop)
+ view.marginTop
+ }
+
+ val initialBottom = if (view.getTag(R.id.initial_margin_bottom) != null) {
+ view.getTag(R.id.initial_margin_bottom) as Int
+ } else {
+ view.setTag(R.id.initial_margin_bottom, view.marginBottom)
+ view.marginBottom
+ }
+
+ val initialLeft = if (view.getTag(R.id.initial_margin_left) != null) {
+ view.getTag(R.id.initial_margin_left) as Int
+ } else {
+ view.setTag(R.id.initial_margin_left, view.marginLeft)
+ view.marginLeft
+ }
+
+ val initialRight = if (view.getTag(R.id.initial_margin_right) != null) {
+ view.getTag(R.id.initial_margin_right) as Int
+ } else {
+ view.setTag(R.id.initial_margin_right, view.marginRight)
+ view.marginRight
+ }
+
+ val accumulator = InsetsAccumulator(
+ initialTop,
+ insets.top,
+ initialBottom,
+ insets.bottom,
+ initialLeft,
+ insets.left,
+ initialRight,
+ insets.right
+ )
+
+ view.updateLayoutParams