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 6fd325813..575aa6a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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 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 7a6a2bcf4..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 = 1058 - versionName = "6.0.2" + 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 e8215bd90..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" /> @@ -101,8 +101,9 @@ android:name=".upload.UploadActivity" android:configChanges="orientation|screenSize|keyboard" 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 1c28d5fe4..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 @@ -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/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/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 e20a75c0f..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 ) } 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 Never ask this again Ask for location permission Ask for location permission when needed for nearby notification card view feature. - Something went wrong, We could not fetch achievements + Something went wrong, and we could not fetch achievements You\'ve made so many contributions our achievements calculation system can\'t cope. This is the ultimate achievement. Ends on: Display campaigns See the ongoing campaigns + Show deletion button + Enable the \"Delete Folder\" button in the custom picker Allow the app to fetch location in case the camera does not record it. Some device cameras do not record location. In such cases, letting the app fetch and attach location to it makes your contribution more useful. You may change this any time from the Settings Allow Dismiss @@ -485,12 +492,12 @@ Upload your first media by tapping on the add button. Could not request category check for %1$s Requesting category check for %1$s Done - Sending Thanks: Success - Successfully sent thanks to %1$s - Failed to send thanks %1$s - Sending Thanks: Failure + Sending thanks: Success + Sent thanks to %1$s + Failed to send thanks to %1$s + Sending thanks: Failure - Sending Thanks for %1$s + Sending thanks for %1$s Does this follow the rules of copyright? Is this correctly categorized? Is this in-scope? @@ -522,15 +529,14 @@ Upload your first media by tapping on the add button. Error occurred while picking images Please wait… - Featured pictures are images from highly skilled photographers and illustrators that the Wikimedia Commons community has chosen as some of the highest quality on the site. Images Uploaded via Nearby places are the images which are uploaded by discovering places on the map. This feature allows editors to send a Thank you notification to users who make useful edits – by using a small thank link on the history page or diff page. - Copy to subsequent media + Copy to the next items Copied Examples of good images to upload to Commons Examples of images not to upload Skip this image - Download Failed!!. We cannot download the file without external storage permission. + Download failed. We cannot download the file without external storage permission. Manage EXIF Tags Select which EXIF tags to keep in uploads @@ -543,12 +549,10 @@ Upload your first media by tapping on the add button. Serial Numbers Software - Media location access denied - We may not be able to automatically obtain location data from pictures you upload. Please add the appropriate location for each picture before submitting - Upload photos to Wikimedia Commons directly from your phone. Download the Commons App now: %1$s Share app via... Image Info + Don\'t show this message again No Categories found No Depictions found Cancelled Upload @@ -605,7 +609,7 @@ Upload your first media by tapping on the add button. Share image via You haven\'t made any contributions yet - %s has not made any contributions yet + %1$s has not made any contributions yet Account created! Text copied to clipboard Notification marked as read @@ -614,7 +618,7 @@ Upload your first media by tapping on the add button. Exists Needs Photo Place type: - Bridge, museum, hotel etc. + Bridge, museum, hotel, etc. Something went wrong with log-in. You must reset your password! MEDIA CHILD CLASSES @@ -662,7 +666,7 @@ Upload your first media by tapping on the add button. 5. Paste the wikitext in the appropriate place. 6. Edit the wikitext for appropriate positioning, if necessary. For more information, see <a href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image">here</a>. 7. Publish the article - Copy wikicode to clipboard + Copy wikitext to clipboard pause resume Paused @@ -705,6 +709,8 @@ Upload your first media by tapping on the add button. Please select the appropriate categories. Unlike depictions, categories are only in English. Commons makes your pictures reusable and adapted by everyone. Do you want to waive all rights? Do you want to be attributed? Do you want adaptations to use the same license? Depicts + Label + Description Media License Media Details View category page @@ -719,8 +725,7 @@ Upload your first media by tapping on the add button. Show in map app Edit location The image view of the location picker - - The shadow of the image view of the location picker + The shadow of the image view of the location picker Image Location Check whether location is correct Label @@ -732,7 +737,7 @@ Upload your first media by tapping on the add button. Back Welcome to Custom Picture Selector This picker shows you which pictures you have already uploaded to Commons. - Unlike the picture on the left, the picture on the right has the Commons logo indicating it is already uploaded. \n Touch and hold for image preview. + Unlike the picture on the left, the picture on the right has the Commons logo indicating it is already uploaded.\n\nTouch and hold for image preview. Awesome This image has already been uploaded to Commons. For technical reasons, the app can\'t reliably upload more than %1$d pictures at once. The upload limit of %1$d has been exceeded by %2$d. @@ -748,9 +753,11 @@ Upload your first media by tapping on the add button. Wiki Loves Monuments is an international photo contest for monuments organised by Wikimedia Need Permission Nearby maps need to read PHONE STATE to function properly + Please turn on location services to view nearby places. + Location access is needed to show nearby places on the map. - Contributions of User: %s - Achievements of User: %s + Contributions of User: %1$s + Achievements of User: %1$s View user profile Edit depictions Edit categories @@ -761,7 +768,7 @@ Upload your first media by tapping on the add button. Location data helps Wiki editors find your picture, making it much more useful.\nYour recent uploads have no location.\nWe suggest you turn on location in your camera app\'s settings.\nThank you for uploading! No location found How about adding the place where this image was taken?\nLocation data helps Wiki editors find your picture, making it much more useful.\nThank you! - Add location + Add Location Please remove from this email any information that you are not comfortable sharing publicly. Also, please be aware that your email address with which you are posting, and the associated name and profile picture, will be visible publicly. Details Achievements are only available in the prod flavor. Please check the developer documentation. @@ -782,8 +789,8 @@ Upload your first media by tapping on the add button. Unmark as not for upload Marking as not for upload Unmarking as not for upload - Show already actioned pictures - Hiding already actioned pictures + Show already handled pictures + Hiding already handled pictures No more images found This image is already uploaded Can not select this image for upload @@ -819,15 +826,15 @@ Upload your first media by tapping on the add button. Your log-in has expired. Please log in again. No application available to open GPX files File Saved Successfully - Do you want to open GPX file? - Do you want to open KML file? - Failed to save KML file. - Failed to save GPX file. - Saving KML File - Saving GPX File + Do you want to open the GPX file? + Do you want to open the KML file? + Failed to save the KML file. + Failed to save the GPX file. + Saving as a KML file... + Saving as a GPX file... - %d image selected - %d images selected + %1$d image selected + %1$d images selected Please remember that all images in a multi-upload get the same categories and depictions. If the images do not share depictions and categories, please perform several separate uploads. Note about multi-uploads @@ -867,7 +874,6 @@ Upload your first media by tapping on the add button. Other wikis File usages - SingleWebViewActivity Account Vanish Account Vanish account warning diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index fb3cb0ca1..3b7604026 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -55,13 +55,13 @@ android:defaultValue="false" android:key="displayDeletionButton" app:singleLineTitle="false" - android:summary="Enable the "Delete folder" button in the custom picker" - android:title="Show Deletion Button" /> + android:summary="@string/show_deletion_button_explanation" + android:title="@string/show_deletion_button" /> + android:title="@string/preferences_uploads"> - - - - - - - - - - - - - - - - - diff --git a/app/src/test/java/android/text/TextUtils.java b/app/src/test/java/android/text/TextUtils.java index 4d63e77df..f59ab806a 100644 --- a/app/src/test/java/android/text/TextUtils.java +++ b/app/src/test/java/android/text/TextUtils.java @@ -21,14 +21,18 @@ public class TextUtils { * mocks TextUtils.equals */ public static boolean equals(CharSequence a, CharSequence b) { - if (a == b) return true; + if (a == b) { + return true; + } int length; if (a != null && b != null && (length = a.length()) == b.length()) { if (a instanceof String && b instanceof String) { return a.equals(b); } else { for (int i = 0; i < length; i++) { - if (a.charAt(i) != b.charAt(i)) return false; + if (a.charAt(i) != b.charAt(i)) { + return false; + } } return true; } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt index 9e7891560..1a9d1500b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt @@ -3,6 +3,10 @@ package fr.free.nrw.commons import com.google.gson.Gson import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.times +import fr.free.nrw.commons.campaigns.CampaignResponseDTO +import fr.free.nrw.commons.campaigns.CampaignConfig +import fr.free.nrw.commons.campaigns.models.Campaign import fr.free.nrw.commons.explore.depictions.DepictsClient import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient @@ -10,13 +14,22 @@ import fr.free.nrw.commons.nearby.model.NearbyQueryParams import okhttp3.Call import okhttp3.HttpUrl import okhttp3.OkHttpClient +import okhttp3.Request import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.mockwebserver.MockResponse +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.times +import org.mockito.Mockito.eq import org.mockito.MockitoAnnotations +import java.io.BufferedReader +import java.io.InputStreamReader import java.lang.Exception class OkHttpJsonApiClientTests { @@ -45,34 +58,43 @@ class OkHttpJsonApiClientTests { @Mock lateinit var response: Response + @Mock + lateinit var responseBody: ResponseBody + + private lateinit var mockWebServer: TestWebServer + @Before fun setUp() { MockitoAnnotations.openMocks(this) - okHttpJsonApiClient = - OkHttpJsonApiClient( - okhttpClient, - depictsClient, - wikiMediaToolforgeUrl, - sparqlQueryUrl, - campaignsUrl, - gson, - ) + mockWebServer = TestWebServer() + mockWebServer.setUp() + okHttpJsonApiClient = OkHttpJsonApiClient( + okhttpClient, + depictsClient, + wikiMediaToolforgeUrl, + sparqlQueryUrl, + mockWebServer.getUrl(), //use the mock server for the campaignsUrl + gson + ) Mockito.`when`(okhttpClient.newCall(any())).thenReturn(call) Mockito.`when`(call.execute()).thenReturn(response) + Mockito.`when`(response.isSuccessful).thenReturn(false) + Mockito.`when`(response.message).thenReturn("test") + Mockito.`when`(response.body).thenReturn(responseBody) + Mockito.`when`(responseBody.string()).thenReturn("{\"error\": \"test\"}") } @Test fun testGetNearbyPlacesCustomQuery() { - Mockito.`when`(response.message).thenReturn("test") try { okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, "test") } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } try { okHttpJsonApiClient.getNearbyPlaces(NearbyQueryParams.Rectangular(latLng, latLng), "test", true, "test") } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } verify(okhttpClient, times(2)).newCall(any()) verify(call, times(2)).execute() @@ -80,11 +102,10 @@ class OkHttpJsonApiClientTests { @Test fun testGetNearbyPlaces() { - Mockito.`when`(response.message).thenReturn("test") try { okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, null) } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } try { okHttpJsonApiClient.getNearbyPlaces( @@ -93,9 +114,8 @@ class OkHttpJsonApiClientTests { true, null ) - } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } try { okHttpJsonApiClient.getNearbyPlaces( @@ -105,7 +125,7 @@ class OkHttpJsonApiClientTests { null ) } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } verify(okhttpClient, times(3)).newCall(any()) verify(call, times(3)).execute() @@ -113,18 +133,121 @@ class OkHttpJsonApiClientTests { @Test fun testGetNearbyItemCount() { - Mockito.`when`(response.message).thenReturn("test") try { okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Radial(latLng, 10f)) } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } try { okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Rectangular(latLng, latLng)) } catch (e: Exception) { - assert(e.message.equals("test")) + assertEquals("test", e.message) } verify(okhttpClient, times(2)).newCall(any()) verify(call, times(2)).execute() } -} + + @Test + fun testGetCampaignsWithData() { + //loads the json response from resources + val jsonResponse = loadJsonFromResource("campaigns_response_with_data.json") + + //mocks the succesfull response chain + Mockito.`when`(response.isSuccessful).thenReturn(true) + Mockito.`when`(response.message).thenReturn("OK") + Mockito.`when`(response.body).thenReturn(responseBody) + Mockito.`when`(responseBody.string()).thenReturn(jsonResponse) + + val campaignResponse = CampaignResponseDTO().apply { + campaignConfig = CampaignConfig().apply { + showOnlyLiveCampaigns = false + sortBy = "startDate" + } + campaigns = listOf( + Campaign().apply { + title = "Wiki Loves Monuments" + isWLMCampaign = true + }, + Campaign().apply { + title = "Wiki Loves Nature" + isWLMCampaign = false + } + ) + } + + //any() for the string argument and eq() for the class argument. + Mockito.`when`( + gson.fromJson( + any(), + eq(CampaignResponseDTO::class.java) + ) + ).thenReturn(campaignResponse) + + //call the getCampaigns + val result: CampaignResponseDTO? = okHttpJsonApiClient.getCampaigns().blockingGet() + + //verify the results + assertNotNull(result) + assertNotNull(result?.campaigns) + assertEquals(2, result?.campaigns!!.size) + assertEquals("Wiki Loves Monuments", result.campaigns!![0].title) + assertTrue(result.campaigns!![0].isWLMCampaign) + assertEquals("Wiki Loves Nature", result.campaigns!![1].title) + assertEquals(false, result.campaigns!![1].isWLMCampaign) + assertNotNull(result.campaignConfig) + assertFalse(result.campaignConfig!!.showOnlyLiveCampaigns) + assertEquals("startDate", result.campaignConfig!!.sortBy) + } + + @Test + fun testGetCampaignsEmpty() { + //loads the empty json response + val jsonResponse = loadJsonFromResource("campaigns_response_empty.json") + + //mocks the successful response chain + Mockito.`when`(response.isSuccessful).thenReturn(true) + Mockito.`when`(response.message).thenReturn("OK") + Mockito.`when`(response.body).thenReturn(responseBody) + Mockito.`when`(responseBody.string()).thenReturn(jsonResponse) + + val campaignResponse = CampaignResponseDTO().apply { + campaignConfig = CampaignConfig().apply { + showOnlyLiveCampaigns = false + sortBy = "startDate" + } + campaigns = emptyList() + } + + //use any() for the string argument and eq() for the class argument. + Mockito.`when`( + gson.fromJson( + any(), + eq(CampaignResponseDTO::class.java) + ) + ).thenReturn(campaignResponse) + + //calls getCampaigns + val result: CampaignResponseDTO? = okHttpJsonApiClient.getCampaigns().blockingGet() + + //verify the results + assertNotNull(result) + assertNotNull(result?.campaigns) + assertTrue(result?.campaigns!!.isEmpty()) + assertNotNull(result.campaignConfig) + assertFalse(result.campaignConfig!!.showOnlyLiveCampaigns) + assertEquals("startDate", result.campaignConfig!!.sortBy) + } + + fun loadJsonFromResource(fileName: String): String { + val resourcePath = "raw/$fileName" + //uses the classloader to find the resource in the test environment + val inputStream = javaClass.classLoader?.getResourceAsStream(resourcePath) + + if (inputStream != null) { + //reads the entire stream content + return BufferedReader(InputStreamReader(inputStream)).use { it.readText() } + } + //throws an exception with the correct expected path + throw IllegalArgumentException("Resource $fileName not found. Please ensure the file is located in app/src/test/resources/raw/") + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt index be3b7e8e3..5b5dfd7dd 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt @@ -193,8 +193,8 @@ class DescriptionEditActivityUnitTest { method.isAccessible = true method.invoke( activity, - android.R.string.ok, - android.R.string.ok, + R.string.ok, + R.string.ok, ) val dialog: AlertDialog = ShadowAlertDialog.getLatestDialog() as AlertDialog assertEquals(dialog.isShowing, true) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt index 75d6b8a4f..55b8427e6 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt @@ -153,6 +153,13 @@ class UploadCategoriesFragmentUnitTests { fragment.showError(R.string.no_categories_found) } + @Test + @Throws(Exception::class) + fun testShowErrorDialog() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.showErrorDialog("") + } + @Test @Throws(Exception::class) fun testSetCategoriesCaseNull() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt index a37bcc927..0cab47c67 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt @@ -145,6 +145,8 @@ class UploadMediaDetailFragmentUnitTest { ibExpandCollapse = view.findViewById(R.id.ib_expand_collapse) Whitebox.setInternalState(fragment, "uploadMediaDetailAdapter", uploadMediaDetailAdapter) + Whitebox.setInternalState(fragment, "defaultKvStore", defaultKvStore) + `when`(defaultKvStore.getString("description_language", "")).thenReturn("en") } @Test @@ -160,16 +162,10 @@ class UploadMediaDetailFragmentUnitTest { fragment.onCreate(Bundle()) } - @Test - @Throws(Exception::class) - fun testSetImageToBeUploaded() { - Shadows.shadowOf(Looper.getMainLooper()).idle() - fragment.setImageToBeUploaded(null, null, location) - } - @Test @Throws(Exception::class) fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() fragment.onCreateView(layoutInflater, null, savedInstanceState) } @@ -181,34 +177,6 @@ class UploadMediaDetailFragmentUnitTest { fragment.onViewCreated(view, savedInstanceState) } - @Test - @Throws(Exception::class) - fun testInit() { - Shadows.shadowOf(Looper.getMainLooper()).idle() - Whitebox.setInternalState(fragment, "presenter", presenter) - val method: Method = - UploadMediaDetailFragment::class.java.getDeclaredMethod( - "initializeFragment", - ) - method.isAccessible = true - method.invoke(fragment) - } - - @Test - @Throws(Exception::class) - fun testInitCaseGetIndexInViewFlipperNonZero() { - Shadows.shadowOf(Looper.getMainLooper()).idle() - Whitebox.setInternalState(fragment, "presenter", presenter) - `when`(callback.getIndexInViewFlipper(fragment)).thenReturn(1) - `when`(callback.totalNumberOfSteps).thenReturn(5) - val method: Method = - UploadMediaDetailFragment::class.java.getDeclaredMethod( - "initializeFragment", - ) - method.isAccessible = true - method.invoke(fragment) - } - @Test @Throws(Exception::class) fun testShowInfoAlert() { @@ -317,7 +285,7 @@ class UploadMediaDetailFragmentUnitTest { @Test @Throws(Exception::class) fun testShowExternalMap() { - shadowOf(Looper.getMainLooper()).idle() + Shadows.shadowOf(Looper.getMainLooper()).idle() `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) `when`(imageCoordinates.decLatitude).thenReturn(0.0) `when`(imageCoordinates.decLongitude).thenReturn(0.0) @@ -328,7 +296,7 @@ class UploadMediaDetailFragmentUnitTest { @Test @Throws(Exception::class) fun testOnCameraPositionCallbackOnMapIconClicked() { - shadowOf(Looper.getMainLooper()).idle() + Shadows.shadowOf(Looper.getMainLooper()).idle() Mockito.mock(LocationPicker::class.java) val intent = Mockito.mock(Intent::class.java) val cameraPosition = Mockito.mock(CameraPosition::class.java) @@ -357,7 +325,7 @@ class UploadMediaDetailFragmentUnitTest { @Test @Throws(Exception::class) fun testOnCameraPositionCallbackAddLocationDialog() { - shadowOf(Looper.getMainLooper()).idle() + Shadows.shadowOf(Looper.getMainLooper()).idle() Mockito.mock(LocationPicker::class.java) val intent = Mockito.mock(Intent::class.java) val cameraPosition = Mockito.mock(CameraPosition::class.java) @@ -427,7 +395,7 @@ class UploadMediaDetailFragmentUnitTest { @Test @Throws(Exception::class) fun testRememberedZoomLevelOnNull() { - shadowOf(Looper.getMainLooper()).idle() + Shadows.shadowOf(Looper.getMainLooper()).idle() Whitebox.setInternalState(fragment, "defaultKvStore", defaultKvStore) `when`(uploadItem.gpsCoords).thenReturn(null) `when`(defaultKvStore.getString(LAST_ZOOM)).thenReturn("13.0") @@ -443,7 +411,7 @@ class UploadMediaDetailFragmentUnitTest { @Test @Throws(Exception::class) fun testRememberedZoomLevelOnNotNull() { - shadowOf(Looper.getMainLooper()).idle() + Shadows.shadowOf(Looper.getMainLooper()).idle() `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) `when`(imageCoordinates.decLatitude).thenReturn(8.0) `when`(imageCoordinates.decLongitude).thenReturn(-8.0) @@ -456,4 +424,4 @@ class UploadMediaDetailFragmentUnitTest { val shadowIntent: ShadowIntent = shadowOf(startedIntent) Assert.assertEquals(shadowIntent.intentClass, LocationPickerActivity::class.java) } -} +} \ No newline at end of file diff --git a/app/src/test/resources/raw/campaigns_response_empty.json b/app/src/test/resources/raw/campaigns_response_empty.json new file mode 100644 index 000000000..61903e24c --- /dev/null +++ b/app/src/test/resources/raw/campaigns_response_empty.json @@ -0,0 +1,7 @@ +{ + "config": { + "showOnlyLiveCampaigns": false, + "sortBy": "startDate" + }, + "campaigns": [] +} \ No newline at end of file diff --git a/app/src/test/resources/raw/campaigns_response_with_data.json b/app/src/test/resources/raw/campaigns_response_with_data.json new file mode 100644 index 000000000..dab818e2a --- /dev/null +++ b/app/src/test/resources/raw/campaigns_response_with_data.json @@ -0,0 +1,24 @@ +{ + "config": { + "showOnlyLiveCampaigns": false, + "sortBy": "startDate" + }, + "campaigns": [ + { + "title": "Wiki Loves Monuments", + "description": "A campaign to photograph monuments", + "startDate": "2025-09-01", + "endDate": "2025-09-30", + "link": "https://commons.wikimedia.org/wiki/Campaign:Wiki_Loves_Monuments", + "isWLMCampaign": true + }, + { + "title": "Wiki Loves Nature", + "description": "A campaign to photograph nature", + "startDate": "2025-06-01", + "endDate": "2025-06-30", + "link": "https://commons.wikimedia.org/wiki/Campaign:Wiki_Loves_Nature", + "isWLMCampaign": false + } + ] +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d26b1a62c..9a4dd53cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -agp = "8.12.0" +agp = "8.13.0" acra = "5.8.4" activityCompose = "1.9.3" adapterdelegates = "4.3.0" androidmediautil = "v1.0-1" -androidSdkVersion = "10.0.1" +androidSdkVersion = "11.13.5" androidPluginScalebar = "1.0.0" androidxJunitVersion = "1.1.5" annotation = "1.3.0" @@ -27,7 +27,7 @@ dexterVersion = "5.0.0" espresso = "3.6.1" exifinterface = "1.3.7" fragmentTesting = "1.6.2" -frescoVersion = "1.13.0" +frescoVersion = "3.6.0" commonsLang3Version = "3.8.1" glide = "4.12.0" gson = "2.8.5" @@ -126,6 +126,7 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = # Image loading facebook-fresco = { module = "com.facebook.fresco:fresco", version.ref = "frescoVersion" } +facebook-fresco-middleware = { module = "com.facebook.fresco:middleware", version.ref = "frescoVersion" } glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlinxCoroutinesRx2" } diff --git a/update-license-info/Makefile b/update-license-info/Makefile deleted file mode 100644 index a6c96ee2a..000000000 --- a/update-license-info/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -.FAKE : build update clean install - -build : ../app/src/main/res/xml/wikimedia_licenses.xml - -../app/src/main/res/xml/wikimedia_licenses.xml : licenses.php mediawiki-extensions-UploadWizard - php licenses.php > ../app/src/main/res/xml/wikimedia_licenses.xml - -mediawiki-extensions-UploadWizard : update - -update : - if [ -d mediawiki-extensions-UploadWizard ]; then (cd mediawiki-extensions-UploadWizard && git pull origin master); else git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/UploadWizard mediawiki-extensions-UploadWizard; fi - -clean : - rm -rf mediawiki-extensions-UploadWizard diff --git a/update-license-info/include-stubs.php b/update-license-info/include-stubs.php deleted file mode 100644 index c0a01a0d6..000000000 --- a/update-license-info/include-stubs.php +++ /dev/null @@ -1,68 +0,0 @@ - 'English' ); - } -} -$wgMemc = new FakeMemc(); - -class FakeMessage { - function plain() { - return 'stub-message-plain'; - } - function parse() { - return 'stub-message-parsed'; - } -} - -function wfMessage() { - return new FakeMessage(); -} - -/** - * Converts shorthand byte notation to integer form - * - * @param $string String - * @return Integer - */ -function wfShorthandToInteger( $string = '' ) { - $string = trim( $string ); - if ( $string === '' ) { - return -1; - } - $last = $string[strlen( $string ) - 1]; - $val = intval( $string ); - switch ( $last ) { - case 'g': - case 'G': - $val *= 1024; - // break intentionally missing - case 'm': - case 'M': - $val *= 1024; - // break intentionally missing - case 'k': - case 'K': - $val *= 1024; - } - - return $val; -} - -$wgAPIModules = array(); diff --git a/update-license-info/licenses.php b/update-license-info/licenses.php deleted file mode 100644 index badda1a08..000000000 --- a/update-license-info/licenses.php +++ /dev/null @@ -1,71 +0,0 @@ - -// 2013-09-30 - -require 'include-stubs.php'; -$config = require "mediawiki-extensions-UploadWizard/UploadWizard.config.php"; -require "mediawiki-extensions-UploadWizard/UploadWizard.i18n.php"; -$licenseList = array(); - -foreach ( $config['licenses'] as $key => $license ) { - // Determine template -> license mappings - if ( isset( $license['templates'] ) ) { - $templates = $license['templates']; - } else { - $templates = array( $key ); - } - - if ( count( $templates ) < 1 ) { - throw new Exception("No templates for $key, this is wrong."); - } - if ( count( $templates ) > 1 ) { - //echo "Skipping multi-template license: $key\n"; - continue; - } - $template = $templates[0]; - if ( preg_match( '/^subst:/i', $template ) ) { - //echo "Skipping subst license: $key\n"; - continue; - } - - $msg = $messages['en'][$license['msg']]; - - $licenseInfo = array( - 'desc' => $msg, - 'template' => $template - ); - if ( isset( $license['url'] ) ) { - $url = $license['url']; - if ( substr( $url, 0, 2 ) == '//' ) { - $url = 'https:' . $url; - } - if ( isset( $license['languageCodePrefix'] ) ) { - $url .= $license['languageCodePrefix'] . '$lang'; - } - $licenseInfo['url'] = $url; - } - $licenseList[$key] = $licenseInfo; -} - -//var_dump( $licenseList ); - -echo "\n"; -echo "\n"; -foreach( $licenseList as $key => $licenseInfo ) { - $encId = htmlspecialchars( $key ); - echo " \n"; - -} -echo "\n"; - \ No newline at end of file