diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index a4682fd3c..dcbba0597 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -70,7 +70,7 @@ body: required: false - type: textarea attributes: - label: Screen-shots + label: Screenshots description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. validations: required: false diff --git a/CHANGELOG.md b/CHANGELOG.md index fc22a2b99..575aa6a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Wikimedia Commons for Android +## v6.0.2 + +### What's changed +* Addressed a bug that prevented the keyboard from appearing in various text fields, such as on the upload wizard +* Links in the "File usages" list are now clickable and will take you to the correct page. +* Titles for file usages are now clearer and easier to understand +* Bug fixes and stability improvements + +## v6.0.1 + +### What's changed +* The app now supports Android 15 with an improved user interface +* Enhanced Nearby with robust and more reliable labels +* Bug fixes and stability improvements + ## v5.6.1 ### What's changed diff --git a/README.md b/README.md index 0b31ff5be..37f1a7872 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,12 @@ Thank you all for your work! | [
misaochan](https://github.com/misaochan) | [
translatewiki](https://github.com/translatewiki) | [
neslihanturan](https://github.com/neslihanturan) | [
yuvipanda](https://github.com/yuvipanda) | [
nicolas-raoul](https://github.com/nicolas-raoul) | | :---: | :---: | :---: | :---: | :---: | -| [
domdomegg](https://github.com/domdomegg) | [
maskaravivek](https://github.com/maskaravivek) | [
psh](https://github.com/psh) | [
madhurgupta10](https://github.com/madhurgupta10) | [
ashishkumar468](https://github.com/ashishkumar468) | -| [
bvibber](https://github.com/bvibber) | [
whym](https://github.com/whym) | [
akaita](https://github.com/akaita) | [
veyndan](https://github.com/veyndan) | [
ujjwalagrawal17](https://github.com/ujjwalagrawal17) | -| [
macgills](https://github.com/macgills) | [
dbrant](https://github.com/dbrant) | [
vanshikaarora](https://github.com/vanshikaarora) | [
sivaraam](https://github.com/sivaraam) | [
Ayan-10](https://github.com/Ayan-10) | -| [
shashankiitbhu](https://github.com/shashankiitbhu) | [
Pratham2305](https://github.com/Pratham2305) | [
sandarumk](https://github.com/sandarumk) | [
tanvidadu](https://github.com/tanvidadu) | [
cypherop](https://github.com/cypherop) | -| [
Prince-kushwaha](https://github.com/Prince-kushwaha) | [
tobias47n9e](https://github.com/tobias47n9e) | [
4D17Y4](https://github.com/4D17Y4) | [
hismaeel](https://github.com/hismaeel) | [
tshradheya](https://github.com/tshradheya) | +| [
psh](https://github.com/psh) | [
domdomegg](https://github.com/domdomegg) | [
maskaravivek](https://github.com/maskaravivek) | [
madhurgupta10](https://github.com/madhurgupta10) | [
ashishkumar468](https://github.com/ashishkumar468) | +| [
bvibber](https://github.com/bvibber) | [
whym](https://github.com/whym) | [
akaita](https://github.com/akaita) | [
sivaraam](https://github.com/sivaraam) | [
veyndan](https://github.com/veyndan) | +| [
ujjwalagrawal17](https://github.com/ujjwalagrawal17) | [
macgills](https://github.com/macgills) | [
amire80](https://github.com/amire80) | [
dbrant](https://github.com/dbrant) | [
vanshikaarora](https://github.com/vanshikaarora) | +| [
RitikaPahwa4444](https://github.com/RitikaPahwa4444) | [
Ayan-10](https://github.com/Ayan-10) | [
rohit9625](https://github.com/rohit9625) | [
shashankiitbhu](https://github.com/shashankiitbhu) | [
Pratham2305](https://github.com/Pratham2305) | +| [
parneet-guraya](https://github.com/parneet-guraya) | [
sandarumk](https://github.com/sandarumk) | [
tanvidadu](https://github.com/tanvidadu) | [
cypherop](https://github.com/cypherop) | [
Prince-kushwaha](https://github.com/Prince-kushwaha) | + .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d85460c45..41788128c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "fr.free.nrw.commons" minSdk = 21 targetSdk = 35 - versionCode = 1056 - versionName = "6.0.0" + versionCode = 1059 + versionName = "6.1.0" setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -226,6 +226,7 @@ dependencies { implementation(libs.rxbinding) implementation(libs.rxbinding.appcompat) implementation(libs.facebook.fresco) + implementation(libs.facebook.fresco.middleware) implementation(libs.apache.commons.lang3) // UI diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d56a874b5..17917666d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,8 +57,7 @@ tools:replace="android:appComponentFactory"> + android:exported="false" /> @@ -85,6 +84,7 @@ android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> @@ -103,7 +103,7 @@ android:exported="true" android:hardwareAccelerated="false" android:icon="@mipmap/ic_launcher" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustPan"> diff --git a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt index d15c72f57..c54c3aefb 100644 --- a/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt +++ b/app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt @@ -1,7 +1,11 @@ package fr.free.nrw.commons import androidx.annotation.VisibleForTesting +import fr.free.nrw.commons.wikidata.GsonUtil import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import fr.free.nrw.commons.wikidata.mwapi.MwErrorResponse +import fr.free.nrw.commons.wikidata.mwapi.MwIOException +import fr.free.nrw.commons.wikidata.mwapi.MwLegacyServiceError import okhttp3.Cache import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -50,7 +54,7 @@ object OkHttpConnectionFactory { } } -private class CommonHeaderRequestInterceptor : Interceptor { +class CommonHeaderRequestInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() @@ -86,16 +90,25 @@ private class UnsuccessfulResponseInterceptor : Interceptor { rsp.peekBody(ERRORS_PREFIX.length.toLong()).use { responseBody -> if (ERRORS_PREFIX == responseBody.string()) { rsp.body.use { body -> - throw IOException(body!!.string()) + val bodyString = body!!.string() + + throw MwIOException( + "MediaWiki API returned error: $bodyString", + GsonUtil.defaultGson.fromJson( + bodyString, + MwErrorResponse::class.java + ).error!!, + ) } } } - } catch (e: IOException) { + } catch (e: MwIOException) { // Log the error as debug (and therefore, "expected") or at error level if (suppressErrors) { Timber.d(e, "Suppressed (known / expected) error") } else { Timber.e(e) + throw e } } return rsp diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt index 688f508ae..0c9901b56 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -89,7 +89,7 @@ class LoginActivity : AccountAuthenticatorActivity() { binding = ActivityLoginBinding.inflate(layoutInflater) applyEdgeToEdgeAllInsets(binding!!.root) - binding?.aboutPrivacyPolicy?.handleKeyboardInsets() + binding!!.root.handleKeyboardInsets() with(binding!!) { setContentView(root) diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt index d64ab16b3..e21e1ac8f 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.kt @@ -144,8 +144,18 @@ class BookmarkItemsDao @Inject constructor( */ @SuppressLint("Range") fun fromCursor(cursor: Cursor) = with(cursor) { + var name = getString(COLUMN_NAME) + if (name == null) { + name = "" + } + + var id = getString(COLUMN_ID) + if (id == null) { + id = "" + } + DepictedItem( - getString(COLUMN_NAME), + name, getString(COLUMN_DESCRIPTION), getString(COLUMN_IMAGE), getStringArray(COLUMN_INSTANCE_LIST), @@ -155,7 +165,7 @@ class BookmarkItemsDao @Inject constructor( getStringArray(COLUMN_CATEGORIES_THUMBNAIL_LIST) ), getString(COLUMN_IS_SELECTED).toBoolean(), - getString(COLUMN_ID) + id ) } @@ -163,19 +173,13 @@ class BookmarkItemsDao @Inject constructor( categoryNameList: List, categoryDescriptionList: List, categoryThumbnailList: List - ): List { - return buildList { - for (i in categoryNameList.indices) { - add( - CategoryItem( - categoryNameList[i], - categoryDescriptionList[i], - categoryThumbnailList[i], - false - ) - ) - } - } + ): List = categoryNameList.mapIndexed { index, name -> + CategoryItem( + name = name, + description = categoryDescriptionList.getOrNull(index), + thumbnail = categoryThumbnailList.getOrNull(index), + isSelected = false + ) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt index e30b3160d..00c8e3228 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.kt @@ -128,7 +128,10 @@ class BookmarkPicturesDao @Inject constructor( } fun fromCursor(cursor: Cursor): Bookmark { - val fileName = cursor.getString(COLUMN_MEDIA_NAME) + var fileName = cursor.getString(COLUMN_MEDIA_NAME) + if (fileName == null) { + fileName = "" + } return Bookmark( fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName) ) diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt index 6bf0bc0ed..9f94e8592 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt @@ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName */ class CampaignConfig { @SerializedName("showOnlyLiveCampaigns") - private val showOnlyLiveCampaigns = false + var showOnlyLiveCampaigns = false @SerializedName("sortBy") - private val sortBy: String? = null -} + var sortBy: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt index 767732eb7..1656109e7 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt @@ -8,8 +8,8 @@ import fr.free.nrw.commons.campaigns.models.Campaign */ class CampaignResponseDTO { @SerializedName("config") - val campaignConfig: CampaignConfig? = null + var campaignConfig: CampaignConfig? = null @SerializedName("campaigns") - val campaigns: List? = null -} + var campaigns: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt index 29267452b..b9532a12e 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt @@ -180,8 +180,8 @@ class ContributionController @Inject constructor(@param:Named("default_preferenc showAlertDialog( activity, activity.getString(R.string.location_permission_title), activity.getString(R.string.in_app_camera_location_permission_rationale), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), + activity.getString(R.string.ok), + activity.getString(R.string.cancel), { createDialogsAndHandleLocationPermissions( activity, diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt index b86cd6dc9..6d0822604 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt @@ -5,7 +5,6 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -20,6 +19,8 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri +import androidx.core.os.BundleCompat import androidx.paging.PagedList import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -38,12 +39,10 @@ import fr.free.nrw.commons.filepicker.FilePicker import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.profile.ProfileActivity import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog -import fr.free.nrw.commons.utils.SystemThemeUtils import fr.free.nrw.commons.utils.ViewUtil.showShortToast import fr.free.nrw.commons.utils.copyToClipboard import fr.free.nrw.commons.utils.handleWebUrl import fr.free.nrw.commons.wikidata.model.WikiSite -import org.apache.commons.lang3.StringUtils import javax.inject.Inject import javax.inject.Named @@ -53,10 +52,6 @@ import javax.inject.Named */ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View, ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback { - @JvmField - @Inject - var systemThemeUtils: SystemThemeUtils? = null - @JvmField @Inject var controller: ContributionController? = null @@ -83,13 +78,14 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL var sessionManager: SessionManager? = null private var binding: FragmentContributionsListBinding? = null - private var fab_close: Animation? = null - private var fab_open: Animation? = null - private var rotate_forward: Animation? = null - private var rotate_backward: Animation? = null + private var fabClose: Animation? = null + private var fabOpen: Animation? = null + private var rotateForward: Animation? = null + private var rotateBackward: Animation? = null private var isFabOpen = false - private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher> + private lateinit var inAppCameraLocationPermissionLauncher: + ActivityResultLauncher> @VisibleForTesting var rvContributionsList: RecyclerView? = null @@ -100,8 +96,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL @VisibleForTesting var callback: Callback? = null - private val SPAN_COUNT_LANDSCAPE = 3 - private val SPAN_COUNT_PORTRAIT = 1 + private val spanCountLandscape = 3 + private val spanCountPortrait = 1 private var contributionsSize = 0 private var userName: String? = null @@ -150,7 +146,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL userName = requireArguments().getString(ProfileActivity.KEY_USERNAME) } - if (StringUtils.isEmpty(userName)) { + if (userName.isNullOrEmpty()) { userName = sessionManager!!.userName } inAppCameraLocationPermissionLauncher = @@ -161,7 +157,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL controller?.locationPermissionCallback?.onLocationPermissionGranted() } else { activity?.let { currentActivity -> - if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { + if (currentActivity.shouldShowRequestPermissionRationale( + permission.ACCESS_FINE_LOCATION)) { controller?.handleShowRationaleFlowCameraLocation( currentActivity, inAppCameraLocationPermissionLauncher, // Pass launcher @@ -169,7 +166,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL ) } else { controller?.locationPermissionCallback?.onLocationPermissionDenied( - currentActivity.getString(R.string.in_app_camera_location_permission_denied) + currentActivity.getString( + R.string.in_app_camera_location_permission_denied) ) } } @@ -189,7 +187,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL contributionsListPresenter!!.onAttachView(this) binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() } binding!!.fabCustomGallery.setOnLongClickListener { view: View? -> - showShortToast(context, fr.free.nrw.commons.R.string.custom_selector_title) + showShortToast(context, R.string.custom_selector_title) true } @@ -199,7 +197,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL } else { binding!!.tvContributionsOfUser.visibility = View.VISIBLE binding!!.tvContributionsOfUser.text = - getString(fr.free.nrw.commons.R.string.contributions_of_user, userName) + getString(R.string.contributions_of_user, userName) binding!!.fabLayout.visibility = View.GONE } @@ -237,7 +235,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL } private fun initAdapter() { - adapter = ContributionsListAdapter(this, mediaClient!!, mediaDataExtractor!!, compositeDisposable) + adapter = ContributionsListAdapter(this, + mediaClient!!, + mediaDataExtractor!!, + compositeDisposable) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -312,7 +313,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { if (e.action == MotionEvent.ACTION_DOWN) { if (isFabOpen) { - animateFAB(isFabOpen) + animateFAB(true) } } return false @@ -344,14 +345,20 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL } private fun getSpanCount(orientation: Int): Int { - return if (orientation == Configuration.ORIENTATION_LANDSCAPE) SPAN_COUNT_LANDSCAPE else SPAN_COUNT_PORTRAIT + return if (orientation == Configuration.ORIENTATION_LANDSCAPE) + spanCountLandscape + else + spanCountPortrait } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // check orientation binding!!.fabLayout.orientation = - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) + LinearLayout.HORIZONTAL + else + LinearLayout.VERTICAL rvContributionsList ?.setLayoutManager( GridLayoutManager(context, getSpanCount(newConfig.orientation)) @@ -359,10 +366,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL } private fun initializeAnimations() { - fab_open = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_open) - fab_close = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_close) - rotate_forward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_forward) - rotate_backward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_backward) + fabOpen = AnimationUtils.loadAnimation(activity, R.anim.fab_open) + fabClose = AnimationUtils.loadAnimation(activity, R.anim.fab_close) + rotateForward = AnimationUtils.loadAnimation(activity, R.anim.rotate_forward) + rotateBackward = AnimationUtils.loadAnimation(activity, R.anim.rotate_backward) } private fun setListeners() { @@ -378,7 +385,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL binding!!.fabCamera.setOnLongClickListener { view: View? -> showShortToast( context, - fr.free.nrw.commons.R.string.add_contribution_from_camera + R.string.add_contribution_from_camera ) true } @@ -387,7 +394,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL animateFAB(isFabOpen) } binding!!.fabGallery.setOnLongClickListener { view: View? -> - showShortToast(context, fr.free.nrw.commons.R.string.menu_from_gallery) + showShortToast(context, R.string.menu_from_gallery) true } } @@ -395,7 +402,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL /** * Launch Custom Selector. */ - protected fun launchCustomSelector() { + private fun launchCustomSelector() { controller!!.initiateCustomGalleryPickWithPermission( requireActivity(), customSelectorLauncherForResult @@ -411,18 +418,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL this.isFabOpen = !isFabOpen if (binding!!.fabPlus.isShown) { if (isFabOpen) { - binding!!.fabPlus.startAnimation(rotate_backward) - binding!!.fabCamera.startAnimation(fab_close) - binding!!.fabGallery.startAnimation(fab_close) - binding!!.fabCustomGallery.startAnimation(fab_close) + binding!!.fabPlus.startAnimation(rotateBackward) + binding!!.fabCamera.startAnimation(fabClose) + binding!!.fabGallery.startAnimation(fabClose) + binding!!.fabCustomGallery.startAnimation(fabClose) binding!!.fabCamera.hide() binding!!.fabGallery.hide() binding!!.fabCustomGallery.hide() } else { - binding!!.fabPlus.startAnimation(rotate_forward) - binding!!.fabCamera.startAnimation(fab_open) - binding!!.fabGallery.startAnimation(fab_open) - binding!!.fabCustomGallery.startAnimation(fab_open) + binding!!.fabPlus.startAnimation(rotateForward) + binding!!.fabCamera.startAnimation(fabOpen) + binding!!.fabGallery.startAnimation(fabOpen) + binding!!.fabCustomGallery.startAnimation(fabOpen) binding!!.fabCamera.show() binding!!.fabGallery.show() binding!!.fabCustomGallery.show() @@ -434,9 +441,9 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL /** * Shows welcome message if user has no contributions yet i.e. new user. */ - override fun showWelcomeTip(shouldShow: Boolean) { + override fun showWelcomeTip(numberOfUploads: Boolean) { binding!!.noContributionsYet.visibility = - if (shouldShow) View.VISIBLE else View.GONE + if (numberOfUploads) View.VISIBLE else View.GONE } /** @@ -456,22 +463,22 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - val layoutManager = rvContributionsList - ?.getLayoutManager() as GridLayoutManager? + val layoutManager = rvContributionsList?.layoutManager as GridLayoutManager? outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState()) } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) if (null != savedInstanceState) { - val savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE) + val savedRecyclerLayoutState = + BundleCompat.getParcelable(savedInstanceState, RV_STATE, Parcelable::class.java) rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState) } } - override fun openMediaDetail(position: Int, isWikipediaButtonDisplayed: Boolean) { + override fun openMediaDetail(contribution: Int, isWikipediaPageExists: Boolean) { if (null != callback) { //Just being safe, ideally they won't be called when detached - callback!!.showDetail(position, isWikipediaButtonDisplayed) + callback!!.showDetail(contribution, isWikipediaPageExists) } } @@ -483,8 +490,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL override fun addImageToWikipedia(contribution: Contribution?) { showAlertDialog( requireActivity(), - getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_title), - getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_desc), + getString(R.string.add_picture_to_wikipedia_article_title), + getString(R.string.add_picture_to_wikipedia_article_desc), { if (contribution != null) { showAddImageToWikipediaInstructions(contribution) @@ -498,16 +505,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL * @param contribution */ private fun showAddImageToWikipediaInstructions(contribution: Contribution) { - val fragmentManager = fragmentManager + val fragmentManager = this.parentFragmentManager val fragment = newInstance(contribution) fragment.callback = - WikipediaInstructionsDialogFragment.Callback { contribution: Contribution?, copyWikicode: Boolean -> - this.onConfirmClicked( + WikipediaInstructionsDialogFragment.Callback { + contribution: Contribution?, + copyWikicode: Boolean -> + onConfirmClicked( contribution, copyWikicode ) } - fragment.show(fragmentManager!!, "WikimediaFragment") + fragment.show(fragmentManager, "WikimediaFragment") } @@ -534,7 +543,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL val url = languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace ?.getWikipediaPageTitle()) - handleWebUrl(requireContext(), Uri.parse(url)) + handleWebUrl(requireContext(), url.toUri()) } fun getContributionStateAt(position: Int): Int { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt index 5c2c44ab5..d481017b2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt @@ -153,21 +153,7 @@ after opening the app. } } setUpPager() - /** - * Ask the user for media location access just after login - * so that location in the EXIF metadata of the images shared by the user - * is retained on devices running Android 10 or above - */ -// if (VERSION.SDK_INT >= VERSION_CODES.Q) { -// ActivityCompat.requestPermissions(this, -// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0); -// PermissionUtils.checkPermissionsAndPerformAction( -// this, -// () -> {}, -// R.string.media_location_permission_denied, -// R.string.add_location_manually, -// permission.ACCESS_MEDIA_LOCATION); -// } + checkAndResumeStuckUploads() } } @@ -338,7 +324,7 @@ after opening the app. ) .subscribeOn(Schedulers.io()) .blockingGet() - Timber.d("Resuming " + stuckUploads.size + " uploads...") + Timber.d("Resuming %d uploads...", stuckUploads.size) if (!stuckUploads.isEmpty()) { for (contribution in stuckUploads) { contribution.state = Contribution.STATE_QUEUED diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt b/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt index 06c31fede..8e899fcba 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt @@ -45,10 +45,10 @@ class SetWallpaperWorker(context: Context, params: WorkerParameters) : } } - override fun onFailureImpl(dataSource: DataSource>?) { + override fun onFailureImpl(dataSource: DataSource?>) { Timber.d("Error getting bitmap from image url %s", imageUrl.toString()) showNotification(context, "Setting Wallpaper Failed", "Failed to download image.") - dataSource?.close() + dataSource.close() } }, CallerThreadExecutor.getInstance()) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt index ec08f6f73..4bf295f4c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt @@ -39,4 +39,11 @@ data class Folder( return true } + + override fun hashCode(): Int { + var result = bucketId.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + images.hashCode() + return result + } } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt index a2965fb5d..a172f28e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.customselector.model import android.net.Uri +import android.os.Build import android.os.Parcel import android.os.Parcelable @@ -48,7 +49,12 @@ data class Image( this( parcel.readLong(), parcel.readString()!!, - parcel.readParcelable(Uri::class.java.classLoader)!!, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + parcel.readParcelable(Uri::class.java.classLoader, Uri::class.java)!! + } else { + @Suppress("DEPRECATION") + parcel.readParcelable(Uri::class.java.classLoader)!! + }, parcel.readString()!!, parcel.readLong(), parcel.readString()!!, @@ -121,4 +127,16 @@ data class Image( override fun newArray(size: Int): Array = arrayOfNulls(size) } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + bucketId.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + uri.hashCode() + result = 31 * result + path.hashCode() + result = 31 * result + bucketName.hashCode() + result = 31 * result + sha1.hashCode() + result = 31 * result + date.hashCode() + return result + } } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 62a440ff4..c3ef4a784 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -168,8 +168,7 @@ class ImageAdapter( // Getting selected index when switch is off } else if (actionableImagesMap.size > position) { - ImageHelper - .getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position]) + ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position]) // For any other case return -1 } else { @@ -348,8 +347,14 @@ class ImageAdapter( numberOfSelectedImagesMarkedAsNotForUpload-- } notifyItemChanged(position, ImageUnselected()) + // Notify listener of deselection to update UI + imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) } else { - val image = images[position] + // Prevent adding the same image multiple times + val image = if (showAlreadyActionedImages) images[position] else ArrayList(actionableImagesMap.values)[position] + if (selectedImages.contains(image)) { + return // Image already selected, ignore additional clicks + } scope.launch(ioDispatcher) { val imageSHA1 = imageLoader.getSHA1(image, defaultDispatcher) withContext(Dispatchers.Main) { @@ -373,7 +378,6 @@ class ImageAdapter( } selectedImages.add(image) notifyItemChanged(position, ImageSelectedOrUpdated()) - imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) } } @@ -632,4 +636,4 @@ class ImageAdapter( fun setSingleSelection(single: Boolean) { singleSelection = single } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index 4f37106cc..a5182fe62 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -9,7 +9,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar -import android.widget.Switch import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible @@ -20,6 +19,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.switchmaterial.SwitchMaterial import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import timber.log.Timber import java.util.TreeMap import javax.inject.Inject import kotlin.collections.ArrayList @@ -81,7 +82,7 @@ class ImageFragment : */ private var selectorRV: RecyclerView? = null private var loader: ProgressBar? = null - private var switch: Switch? = null + private var switch: SwitchMaterial? = null lateinit var filteredImages: ArrayList /** @@ -211,8 +212,12 @@ class ImageFragment : savedInstanceState: Bundle?, ): View? { _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) - imageAdapter = - ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) + + // ensures imageAdapter is initialized + if (!::imageAdapter.isInitialized) { + imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) + Timber.d("Initialized imageAdapter in onCreateView") + } // Set single selection mode if needed val singleSelection = (activity as? CustomSelectorActivity)?.intent?.getBooleanExtra(CustomSelectorActivity.EXTRA_SINGLE_SELECTION, false) == true imageAdapter.setSingleSelection(singleSelection) @@ -370,7 +375,12 @@ class ImageFragment : * notifyDataSetChanged, rebuild the holder views to account for deleted images. */ override fun onResume() { - imageAdapter.notifyDataSetChanged() + if (::imageAdapter.isInitialized) { + imageAdapter.notifyDataSetChanged() + Timber.d("Notified imageAdapter in onResume") + } else { + Timber.w("imageAdapter not initialized in onResume") + } super.onResume() } @@ -380,14 +390,19 @@ class ImageFragment : * Save the Image Fragment state. */ override fun onDestroy() { - imageAdapter.cleanUp() + if (::imageAdapter.isInitialized) { + imageAdapter.cleanUp() + Timber.d("Cleaned up imageAdapter in onDestroy") + } else { + Timber.w("imageAdapter not initialized in onDestroy, skipping cleanup") + } val position = - (selectorRV?.layoutManager as GridLayoutManager) - .findFirstVisibleItemPosition() + (selectorRV?.layoutManager as? GridLayoutManager) + ?.findFirstVisibleItemPosition() ?: -1 - // Check for empty RecyclerView. - if (position != -1 && filteredImages.size > 0) { + // check for valid position and non-empty image list + if (position != -1 && filteredImages.isNotEmpty() && ::imageAdapter.isInitialized) { context?.let { context -> context .getSharedPreferences( @@ -396,34 +411,57 @@ class ImageFragment : )?.let { prefs -> prefs.edit()?.let { editor -> editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() + Timber.d("Saved last visible item ID: %d", imageAdapter.getImageIdAt(position)) } } } + } else { + Timber.d("Skipped saving item ID: position=%d, filteredImages.size=%d, imageAdapter initialized=%b", + position, filteredImages.size, ::imageAdapter.isInitialized) } super.onDestroy() } override fun onDestroyView() { _binding = null + selectorRV = null + loader = null + switch = null + progressLayout = null super.onDestroyView() } override fun refresh() { - imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) + if (::imageAdapter.isInitialized) { + imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) + Timber.d("Refreshed imageAdapter") + } else { + Timber.w("imageAdapter not initialized in refresh") + } } /** * Removes the image from the actionable image map */ fun removeImage(image: Image) { - imageAdapter.removeImageFromActionableImageMap(image) + if (::imageAdapter.isInitialized) { + imageAdapter.removeImageFromActionableImageMap(image) + Timber.d("Removed image from actionable image map") + } else { + Timber.w("imageAdapter not initialized in removeImage") + } } /** * Clears the selected images */ fun clearSelectedImages() { - imageAdapter.clearSelectedImages() + if (::imageAdapter.isInitialized) { + imageAdapter.clearSelectedImages() + Timber.d("Cleared selected images") + } else { + Timber.w("imageAdapter not initialized in clearSelectedImages") + } } /** @@ -434,6 +472,15 @@ class ImageFragment : selectedImages: ArrayList, shouldRefresh: Boolean, ) { + if (::imageAdapter.isInitialized) { + imageAdapter.setSelectedImages(selectedImages) + if (shouldRefresh) { + imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) + } + Timber.d("Passed %d selected images to imageAdapter, shouldRefresh=%b", selectedImages.size, shouldRefresh) + } else { + Timber.w("imageAdapter not initialized in passSelectedImages") + } } /** @@ -443,6 +490,7 @@ class ImageFragment : if (!progressDialog.isShowing) { progressDialogLayout.progressDialogText.text = text progressDialog.show() + Timber.d("Showing mark/unmark progress dialog: %s", text) } } @@ -452,6 +500,7 @@ class ImageFragment : fun dismissMarkUnmarkProgressDialog() { if (progressDialog.isShowing) { progressDialog.dismiss() + Timber.d("Dismissed mark/unmark progress dialog") } } @@ -461,4 +510,4 @@ class ImageFragment : listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED), )?.subscribeOn(Schedulers.io()) ?.blockingGet() ?: emptyList() -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 89d43845b..b1f1b7f9b 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -150,7 +150,7 @@ class DescriptionEditActivity : this, getString(titleStringID), getString(messageStringId), - getString(android.R.string.ok), + getString(R.string.ok), null ) } diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt index 2539db312..9246ff303 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -7,6 +7,7 @@ import dagger.Provides import fr.free.nrw.commons.BetaConstants import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.OkHttpConnectionFactory +import fr.free.nrw.commons.CommonHeaderRequestInterceptor import fr.free.nrw.commons.actions.PageEditClient import fr.free.nrw.commons.actions.PageEditInterface import fr.free.nrw.commons.actions.ThanksInterface @@ -60,6 +61,7 @@ class NetworkingModule { .connectTimeout(120, TimeUnit.SECONDS) .writeTimeout(120, TimeUnit.SECONDS) .addInterceptor(httpLoggingInterceptor) + .addInterceptor(CommonHeaderRequestInterceptor()) .readTimeout(120, TimeUnit.SECONDS) .cache(Cache(File(context.cacheDir, "okHttpCache"), OK_HTTP_CACHE_SIZE)) .build() diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt index ea96b50a3..bc8f9cfaa 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt @@ -64,6 +64,9 @@ class ExploreFragment : CommonsDaggerSupportFragment() { override fun onPageScrollStateChanged(state: Int) = Unit override fun onPageSelected(position: Int) { binding!!.viewPager.canScroll = position != 2 + if (position == 2) { + mapRootFragment?.requestLocationIfNeeded() + } } }) setTabs() @@ -171,14 +174,12 @@ class ExploreFragment : CommonsDaggerSupportFragment() { // if on Map tab, show all menu options, else only show search binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit - + override fun onPageScrollStateChanged(state: Int) = Unit override fun onPageSelected(position: Int) { - others.setVisible((position == 2)) - } - - override fun onPageScrollStateChanged(state: Int) { - if (state == ViewPager.SCROLL_STATE_IDLE && binding!!.viewPager.currentItem == 2) { - onPageSelected(2) + binding!!.viewPager.canScroll = position != 2 + others.setVisible(position == 2) + if (position == 2) { + mapRootFragment?.requestLocationIfNeeded() } } }) @@ -194,7 +195,6 @@ class ExploreFragment : CommonsDaggerSupportFragment() { */ override fun onOptionsItemSelected(item: MenuItem): Boolean { // Handle item selection - when (item.itemId) { R.id.action_search -> { startActivityWithFlags(requireActivity(), SearchActivity::class.java) @@ -224,6 +224,4 @@ class ExploreFragment : CommonsDaggerSupportFragment() { retainInstance = true } } -} - - +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt index af65834eb..d405709a8 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt @@ -193,9 +193,20 @@ class ExploreMapRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider binding = null } + fun requestLocationIfNeeded() { + mapFragment?.requestLocationIfNeeded() + } + + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + if (isVisibleToUser) { + requestLocationIfNeeded() + } + } + companion object { fun newInstance(): ExploreMapRootFragment = ExploreMapRootFragment().apply { retainInstance = true } } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt index 32af67e95..d025fdfe1 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt @@ -112,8 +112,8 @@ class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, Categor viewPagerAdapter!!.setTabs( R.string.title_for_media to depictionImagesListFragment!!, - R.string.title_for_subcategories to childDepictionsFragment, - R.string.title_for_parent_categories to parentDepictionsFragment + R.string.title_for_child_classes to childDepictionsFragment, + R.string.title_for_parent_classes to parentDepictionsFragment ) binding!!.viewPager.offscreenPageLimit = 2 viewPagerAdapter!!.notifyDataSetChanged() diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt index e64f12db3..a1bae09fb 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt @@ -140,8 +140,8 @@ class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.Vi requireActivity(), requireActivity().getString(R.string.location_permission_title), requireActivity().getString(R.string.location_permission_rationale_explore), - requireActivity().getString(android.R.string.ok), - requireActivity().getString(android.R.string.cancel), + requireActivity().getString(R.string.ok), + requireActivity().getString(R.string.cancel), { askForLocationPermission() }, null, null @@ -269,31 +269,60 @@ class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.Vi override fun onZoom(event: ZoomEvent?): Boolean = false }) - if (!locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { - askForLocationPermission() - } + // removed tha permission check here to prevent it from running on fragment creation } override fun onResume() { super.onResume() binding!!.mapView.onResume() presenter!!.attachView(this) - registerNetworkReceiver() - if (isResumed) { - if (locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { - performMapReadyActions() - } else { - startMapWithoutPermission() - } + locationManager.addLocationListener(this) + if (broadcastReceiver != null) { + requireActivity().registerReceiver(broadcastReceiver, intentFilter) } + setSearchThisAreaButtonVisibility(false) } override fun onPause() { super.onPause() // unregistering the broadcastReceiver, as it was causing an exception and a potential crash unregisterNetworkReceiver() + locationManager.unregisterLocationManager() + locationManager.removeLocationListener(this) } + fun requestLocationIfNeeded() { + if (!isVisible) return // skips if not visible to user + if (locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + if (locationPermissionsHelper!!.isLocationAccessToAppsTurnedOn()) { + locationManager.registerLocationManager() + drawMyLocationMarker() + } else { + locationPermissionsHelper!!.showLocationOffDialog(requireActivity(), R.string.location_off_dialog_text) + } + } else { + locationPermissionsHelper!!.requestForLocationAccess( + R.string.location_permission_title, + R.string.location_permission_rationale + ) + } + } + + private fun drawMyLocationMarker() { + val location = locationManager.getLastLocation() + if (location != null) { + val geoPoint = GeoPoint(location.latitude, location.longitude) + val startMarker = Marker(binding!!.mapView).apply { + setPosition(geoPoint) + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + icon = ContextCompat.getDrawable(requireContext(), R.drawable.current_location_marker) + title = "Your Location" + textLabelFontSize = 24 + } + binding!!.mapView.overlays.add(startMarker) + binding!!.mapView.invalidate() + } + } /** * Unregisters the networkReceiver @@ -936,13 +965,17 @@ class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.Vi if (geoPoint != null) { binding!!.mapView.controller.setCenter(geoPoint) val overlays = binding!!.mapView.overlays + // collects the indices of items to remove + val indicesToRemove = mutableListOf() for (i in overlays.indices) { - if (overlays[i] is Marker) { - binding!!.mapView.overlays.removeAt(i) - } else if (overlays[i] is ScaleDiskOverlay) { - binding!!.mapView.overlays.removeAt(i) + if (overlays[i] is Marker || overlays[i] is ScaleDiskOverlay) { + indicesToRemove.add(i) } } + // removes the items in reverse order to avoid index shifting + indicesToRemove.sortedDescending().forEach { index -> + binding!!.mapView.overlays.removeAt(index) + } val diskOverlay = ScaleDiskOverlay( requireContext(), geoPoint, 2000, GeoConstants.UnitOfMeasure.foot @@ -952,7 +985,6 @@ class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.Vi this.style = Paint.Style.STROKE this.strokeWidth = 2f }) - setCirclePaint1(Paint().apply { setColor(Color.argb(40, 128, 128, 128)) this.style = Paint.Style.FILL_AND_STROKE @@ -961,7 +993,6 @@ class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.Vi setDisplaySizeMax(1700) } binding!!.mapView.overlays.add(diskOverlay) - val startMarker = Marker( binding!!.mapView ).apply { @@ -1079,7 +1110,24 @@ class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.Vi override fun onLocationPermissionDenied(toastMessage: String) = Unit - override fun onLocationPermissionGranted() = Unit + override fun onLocationPermissionGranted() { + if (locationPermissionsHelper!!.isLocationAccessToAppsTurnedOn()) { + locationManager.registerLocationManager() + drawMyLocationMarker() + } else { + locationPermissionsHelper!!.showLocationOffDialog(requireActivity(), R.string.location_off_dialog_text) + } + onLocationChanged(LocationChangeType.PERMISSION_JUST_GRANTED, null) + } + + fun onLocationChanged(locationChangeType: LocationChangeType, location: Location?) { + if (locationChangeType == LocationChangeType.PERMISSION_JUST_GRANTED) { + val curLatLng = locationManager.getLastLocation() ?: getMapCenter() + populatePlaces(curLatLng) + } else { + presenter!!.updateMap(locationChangeType) + } + } companion object { fun newInstance(): ExploreMapFragment { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt index e1d0740de..d16d250dd 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt @@ -163,11 +163,19 @@ class RecentSearchesDao @Inject constructor( * @param cursor * @return RecentSearch object */ - fun fromCursor(cursor: Cursor): RecentSearch = RecentSearch( - uriForId(cursor.getInt(COLUMN_ID)), - cursor.getString(COLUMN_NAME), - Date(cursor.getLong(COLUMN_LAST_USED)) - ) + fun fromCursor(cursor: Cursor): RecentSearch { + var query = cursor.getString(COLUMN_NAME) + + if (query == null) { + query = "" + } + + return RecentSearch( + uriForId(cursor.getInt(COLUMN_ID)), + query, + Date(cursor.getLong(COLUMN_LAST_USED)) + ) + } /** * This class contains the database table architechture for recent searches, diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt index c0f1bd5db..e7903c9ed 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt @@ -67,10 +67,10 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { private fun showDeleteRecentAlertDialog(context: Context) { AlertDialog.Builder(context) .setMessage(getString(R.string.delete_recent_searches_dialog)) - .setPositiveButton(android.R.string.yes) { dialog: DialogInterface, _: Int -> + .setPositiveButton(R.string.yes) { dialog: DialogInterface, _: Int -> setDeleteRecentPositiveButton(context, dialog) } - .setNegativeButton(android.R.string.no, null) + .setNegativeButton(R.string.no, null) .setCancelable(false) .create() .show() @@ -102,7 +102,7 @@ class RecentSearchesFragment : CommonsDaggerSupportFragment() { setDeletePositiveButton(context, dialog, position) } ) - .setNegativeButton(android.R.string.cancel, null) + .setNegativeButton(R.string.cancel, null) .setCancelable(false) .create() .show() diff --git a/app/src/main/java/fr/free/nrw/commons/fileusages/FileUsagesUiModel.kt b/app/src/main/java/fr/free/nrw/commons/fileusages/FileUsagesUiModel.kt index 63b0740d0..540c87e4c 100644 --- a/app/src/main/java/fr/free/nrw/commons/fileusages/FileUsagesUiModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/fileusages/FileUsagesUiModel.kt @@ -1,18 +1,68 @@ package fr.free.nrw.commons.fileusages +import android.net.Uri +import timber.log.Timber + /** - * Show where file is being used on Commons and oher wikis. + * Data model for displaying file usage information in the UI, including the title and link to the page. */ data class FileUsagesUiModel( val title: String, val link: String? ) +/** + * Converts a FileUsage object to a UI model for Commons file usages. + * Creates a link to the file's page on Commons. + */ fun FileUsage.toUiModel(): FileUsagesUiModel { - return FileUsagesUiModel(title = title, link = "https://commons.wikimedia.org/wiki/$title") + // Replace spaces with underscores and URL-encode the title for the link + val encodedTitle = Uri.encode(title.replace(" ", "_")) + return FileUsagesUiModel( + title = title, + link = "https://commons.wikimedia.org/wiki/$encodedTitle" + ) } +/** + * Converts a GlobalFileUsage object to a UI model for file usages on other wikis. + * Generates a link to the page and prefixes the title with the wiki code (e.g., "(en) Title"). + */ fun GlobalFileUsage.toUiModel(): FileUsagesUiModel { - // link is associated with sub items under wiki group (which is not used ATM) - return FileUsagesUiModel(title = wiki, link = null) -} + // Log input values for debugging + Timber.d("Converting GlobalFileUsage: wiki=$wiki, title=$title") + + // Check for invalid or empty inputs + if (wiki.isBlank() || title.isBlank()) { + Timber.w("Invalid input: wiki=$wiki, title=$title") + return FileUsagesUiModel(title = title, link = null) + } + + // Extract wiki code for prefix (e.g., "en" from "en.wikipedia.org" or "enwiki") + val wikiCode = when { + wiki.contains(".") -> wiki.substringBefore(".") // e.g., "en" from "en.wikipedia.org" + wiki == "commonswiki" -> "commons" + wiki.endsWith("wiki") -> wiki.removeSuffix("wiki") + else -> wiki + } + + // Create prefixed title, e.g., "(en) Changi East Depot" + val prefixedTitle = "($wikiCode) $title" + + // Determine the domain for the URL + val domain = when { + wiki.contains(".") -> wiki // Already a full domain, e.g., "en.wikipedia.org" + wiki == "commonswiki" -> "commons.wikimedia.org" + wiki.endsWith("wiki") -> wiki.removeSuffix("wiki") + ".wikipedia.org" + else -> "$wiki.wikipedia.org" // Fallback for simple codes like "en" + } + + // Normalize title: replace spaces with underscores and URL-encode + val encodedTitle = Uri.encode(title.replace(" ", "_")) + + // Build the full URL + val url = "https://$domain/wiki/$encodedTitle" + Timber.d("Generated URL: $url") + + return FileUsagesUiModel(title = prefixedTitle, link = url) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt index fefb59adb..47b4165ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt @@ -64,8 +64,8 @@ class LocationPermissionsHelper( activity, activity.getString(dialogTitleResource), activity.getString(dialogTextResource), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), + activity.getString(R.string.ok), + activity.getString(R.string.cancel), { ActivityCompat.requestPermissions( activity, diff --git a/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt index e4fedf2e4..08dee587b 100644 --- a/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/locationpicker/LocationPickerActivity.kt @@ -46,6 +46,7 @@ import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Compani import fr.free.nrw.commons.utils.DialogUtil import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets +import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets import fr.free.nrw.commons.utils.handleGeoCoordinates import io.reactivex.android.schedulers.AndroidSchedulers @@ -342,6 +343,10 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { } private fun setupMapView() { + + val mapBottomLayout: ConstraintLayout = findViewById(R.id.map_bottom_layout) + mapBottomLayout.applyEdgeToEdgeBottomPaddingInsets() + requestLocationPermissions() //If location metadata is available, move map to that location. 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 d34c162dc..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 @@ -541,6 +541,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C } ) binding.progressBarEdit.visibility = View.GONE + binding.descriptionEdit.visibility = View.VISIBLE } override fun onConfigurationChanged(newConfig: Configuration) { @@ -1026,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 ) } @@ -2128,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, @@ -2157,11 +2153,8 @@ fun FileUsagesContainer( MediaDetailViewModel.FileUsagesContainerState.Loading -> { LinearProgressIndicator() } - is MediaDetailViewModel.FileUsagesContainerState.Success -> { - val data = commonsContainerState.data - if (data.isNullOrEmpty()) { ListItem(headlineContent = { Text( @@ -2181,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( @@ -2189,11 +2182,11 @@ fun FileUsagesContainer( textDecoration = TextDecoration.Underline ) ) - }) + } + ) } } } - is MediaDetailViewModel.FileUsagesContainerState.Error -> { ListItem(headlineContent = { Text( @@ -2203,12 +2196,10 @@ fun FileUsagesContainer( ) }) } - MediaDetailViewModel.FileUsagesContainerState.Initial -> {} } } - Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -2219,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, @@ -2236,11 +2224,8 @@ fun FileUsagesContainer( MediaDetailViewModel.FileUsagesContainerState.Loading -> { LinearProgressIndicator() } - is MediaDetailViewModel.FileUsagesContainerState.Success -> { - val data = globalContainerState.data - if (data.isNullOrEmpty()) { ListItem(headlineContent = { Text( @@ -2259,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( @@ -2278,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/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