diff --git a/CHANGELOG.md b/CHANGELOG.md index 59134b236..e7accf82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,89 @@ # Wikimedia Commons for Android +## v5.1.2 + +### What's changed + +* Fix the broken category search in the explore screen + +## v5.1.1 + +### What's changed + +* Use Android's new EXIF interface to mitigate security issues in old + EXIF interface. +* Make the icon that helps view the upload queue always visible as it ensures + that the queue accessible at all times. + +## v5.1.0 + +### What's Changed + +* Enhanced **upload queue management** in the Commons app for smoother, sequential + processing, clearer progress tracking, prevention of stuck or duplicate + uploads. As part of this improvement, the "Limited Connection mode" has been + removed. +* Added an option in "Nearby" feature enabling users to **provide feedback on + Wikidata items**. Users can report if an item doesn’t exist, is at a different + location, or has other issues, with submissions tagged for easy tracking and + updates. +* Improved the "Nearby" feature by splitting the query into two parts for faster + loading and **better performance, especially in areas with dense amount of + places**. This update also resolves issues with pins overlapping place names. +* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to + the app such as adding **"Partial Access" support**. Also includes some minor + refactoring, and replacement of deprecated circular progress bars. +* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs + appeared blank** in the Category Details screen. Resolved by optimizing view + binding handling in the parent fragments. +* Fixed an issue where editing depictions removed all other structured data from + images. Now, **only depictions are updated, preserving other associated data**. +* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location** + from pictures and ensured "Show in map app" accurately reflects this location. +* Fixed navigation **after uploading via Nearby by directing users to the Uploads + activity** instead of returning to Nearby, preventing confusion about needing to + upload again. + +### Bug fixes and various changes + +* Improved the "Nearby" feature to fetch labels based on the user's preferred + language instead of defaulting to English. +* Added a legend to the "Nearby" feature indicating pin statuses: red for items + without pictures, green for those with pictures, and grey for items being + checked. A floating action button now allows users to toggle the legend's + visibility. +* Fixed an issue where the "Nominate for deletion" option is shown to logged out + users, preventing app errors and crashes. +* Updated the regex pattern that filters categories with an year in it to also + filter the 2020s. +* Fix an issue where past depictions were not shown as suggestions, despite + being saved correctly. +* Fixed an issue in custom image picker where exiting the media preview showed + only the first image and cleared selections. Now, previously selected images + are restored correctly after exiting the preview. This was contributed. +* Fixed an issue in custom image picker where scrolling behavior did not + maintain position after exiting fullscreen preview, ensuring users remain at + the same point in their image roll unless actioned images are filtered. This + was contributed. +* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll + threshold and adding an 800ms debounce for smoother pin updates when the map + is moved. Queued searches are now canceled on fragment destruction. +* Revised author information retrieval to emphasize the custom author name from + the metadata instead of the default registered username. +* Enhanced notification classification to properly identify "email" type + notifications and prompting users to check their e-mail inbox when such + notifications are clicked. +* Resolved a bug in the language chooser that incorrectly greyed-out previously + selected languages, ensuring only the current language is non-selectable during + image upload. +* Resolved pin color update issue in "Nearby" feature where the pin colour + failed to be updated after a successful image upload. + +What's listed here is only a subset of all the changes. Check the full-list of +the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0). +Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0) +for an exhaustive list of changes and the various contributors who contributed the same. + ## v5.0.2 - Enhanced multi-upload functionality with user prompts to clarify that all images would share the diff --git a/app/build.gradle b/app/build.gradle index f50a4e6dd..2bde0d4f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -212,8 +212,8 @@ android { defaultConfig { //applicationId 'fr.free.nrw.commons' - versionCode 1040 - versionName '5.0.2' + versionCode 1043 + versionName '5.1.2' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion 21 @@ -314,6 +314,7 @@ android { buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"" + buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"" @@ -350,6 +351,7 @@ android { buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"" + buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e7c64f929..fb776920e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -262,4 +262,4 @@ android:required="false" /> - \ No newline at end of file + diff --git a/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt index 284c84caf..0583ae2f9 100644 --- a/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.webkit.ConsoleMessage +import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebView @@ -28,13 +29,20 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import fr.free.nrw.commons.R +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import okhttp3.HttpUrl.Companion.toHttpUrl import timber.log.Timber +import javax.inject.Inject /** * SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and * closes itself when a specified success URL is reached to success url. */ class SingleWebViewActivity : ComponentActivity() { + @Inject + lateinit var cookieJar: CommonsCookieJar + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,6 +52,11 @@ class SingleWebViewActivity : ComponentActivity() { finish() return } + ApplicationlessInjection + .getInstance(applicationContext) + .commonsApplicationComponent + .inject(this) + setCookies(url) enableEdgeToEdge() setContent { Scaffold( @@ -131,6 +144,7 @@ class SingleWebViewActivity : ComponentActivity() { override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) + setCookies(url.orEmpty()) } } @@ -152,6 +166,20 @@ class SingleWebViewActivity : ComponentActivity() { } + /** + * Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`. + * + * @param url The URL for which cookies need to be set. + */ + private fun setCookies(url: String) { + CookieManager.getInstance().let { + val cookies = cookieJar.loadForRequest(url.toHttpUrl()) + for (cookie in cookies) { + it.setCookie(url, cookie.toString()) + } + } + } + companion object { private const val VANISH_ACCOUNT_URL = "VanishAccountUrl" private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl" diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt index 7e6fee2fc..fd90be95f 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.kt @@ -127,30 +127,64 @@ class CategoriesModel /** * Fetches details of every category associated with selected depictions, converts them into * CategoryItem and returns them in a list. + * If a selected depiction has no categories, the categories in which its P18 belongs are + * returned in the list. * * @param selectedDepictions selected DepictItems * @return List of CategoryItem associated with selected depictions */ - private fun categoriesFromDepiction(selectedDepictions: List): Observable>? = - Observable - .fromIterable( - selectedDepictions.map { it.commonsCategories }.flatten(), - ).map { categoryItem -> - categoryClient - .getCategoriesByName( - categoryItem.name, - categoryItem.name, - SEARCH_CATS_LIMIT, - ).map { - CategoryItem( - it[0].name, - it[0].description, - it[0].thumbnail, - it[0].isSelected, - ) - }.blockingGet() - }.toList() - .toObservable() + private fun categoriesFromDepiction(selectedDepictions: List): Observable>? { + val observables = selectedDepictions.map { depictedItem -> + if (depictedItem.commonsCategories.isEmpty()) { + if (depictedItem.primaryImage == null) { + return@map Observable.just(emptyList()) + } + Observable.just( + depictedItem.primaryImage + ).map { image -> + categoryClient + .getCategoriesOfImage( + image, + SEARCH_CATS_LIMIT, + ).map { + it.map { category -> + CategoryItem( + category.name, + category.description, + category.thumbnail, + category.isSelected, + ) + } + }.blockingGet() + }.flatMapIterable { it }.toList() + .toObservable() + } else { + Observable + .fromIterable( + depictedItem.commonsCategories, + ).map { categoryItem -> + categoryClient + .getCategoriesByName( + categoryItem.name, + categoryItem.name, + SEARCH_CATS_LIMIT, + ).map { + CategoryItem( + it[0].name, + it[0].description, + it[0].thumbnail, + it[0].isSelected, + ) + }.blockingGet() + }.toList() + .toObservable() + } + } + return Observable.concat(observables) + .scan(mutableListOf()) { accumulator, currentList -> + accumulator.apply { addAll(currentList) } + } + } /** * Fetches details of every category by their name, converts them into diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 5571e0ea7..b031f12f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -78,6 +78,24 @@ class CategoryClient ), ) + /** + * Fetches categories belonging to an image (P18 of some wikidata entity). + * + * @param image P18 of some wikidata entity + * @param itemLimit How many categories to return + * @return Single Observable emitting the list of categories + */ + fun getCategoriesOfImage( + image: String, + itemLimit: Int, + ): Single> = + responseMapper( + categoryInterface.getCategoriesByTitles( + "File:${image}", + itemLimit, + ), + ) + /** * The method takes categoryName as input and returns a List of Subcategories * It uses the generator query API to get the subcategories in a category, 500 at a time. diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt index aa5ac8c3a..3888ef889 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryInterface.kt @@ -61,6 +61,21 @@ interface CategoryInterface { @Query("gacoffset") offset: Int, ): Single + /** + * Fetches non-hidden categories by titles. + * + * @param titles titles to fetch categories for (e.g. File:) + * @param itemLimit How many categories to return + * @return MwQueryResponse + */ + @GET( + "w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70&gclshow=!hidden", + ) + fun getCategoriesByTitles( + @Query("titles") titles: String?, + @Query("gcllimit") itemLimit: Int, + ): Single + @GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50") fun getSubCategoryList( @Query("gcmtitle") categoryName: String, diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt index 92fab56af..d623730ab 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt @@ -101,7 +101,7 @@ data class Contribution constructor( */ fun formatCaptions(uploadMediaDetails: List) = uploadMediaDetails - .associate { it.languageCode!! to it.captionText!! } + .associate { it.languageCode!! to it.captionText } .filter { it.value.isNotBlank() } /** 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 74b937f97..ff623d496 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 @@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import java.util.TreeMap import kotlin.collections.ArrayList @@ -103,6 +104,18 @@ class ImageAdapter( */ private var imagePositionAsPerIncreasingOrder = 0 + /** + * Stores the number of images currently visible on the screen + */ + private val _currentImagesCount = MutableStateFlow(0) + val currentImagesCount = _currentImagesCount + + /** + * Stores whether images are being loaded or not + */ + private val _isLoadingImages = MutableStateFlow(false) + val isLoadingImages = _isLoadingImages + /** * Coroutine Dispatchers and Scope. */ @@ -184,8 +197,12 @@ class ImageAdapter( // If the position is not already visited, that means the position is new then // finds the next actionable image position from all images if (!alreadyAddedPositions.contains(position)) { - processThumbnailForActionedImage(holder, position, uploadingContributionList) - + processThumbnailForActionedImage( + holder, + position, + uploadingContributionList + ) + _isLoadingImages.value = false // If the position is already visited, that means the image is already present // inside map, so it will fetch the image from the map and load in the holder } else { @@ -231,6 +248,7 @@ class ImageAdapter( position: Int, uploadingContributionList: List, ) { + _isLoadingImages.value = true val next = imageLoader.nextActionableImage( allImages, @@ -252,6 +270,7 @@ class ImageAdapter( actionableImagesMap[next] = allImages[next] alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder) imagePositionAsPerIncreasingOrder++ + _currentImagesCount.value = imagePositionAsPerIncreasingOrder Glide .with(holder.image) .load(allImages[next].uri) @@ -267,6 +286,7 @@ class ImageAdapter( reachedEndOfFolder = true notifyItemRemoved(position) } + _isLoadingImages.value = false } /** @@ -372,6 +392,7 @@ class ImageAdapter( emptyMap: TreeMap, uploadedImages: List = ArrayList(), ) { + _isLoadingImages.value = true allImages = fixedImages val oldImageList: ArrayList = images val newImageList: ArrayList = ArrayList(newImages) @@ -382,6 +403,7 @@ class ImageAdapter( reachedEndOfFolder = false selectedImages = ArrayList() imagePositionAsPerIncreasingOrder = 0 + _currentImagesCount.value = imagePositionAsPerIncreasingOrder val diffResult = DiffUtil.calculateDiff( ImagesDiffCallback(oldImageList, newImageList), @@ -441,6 +463,7 @@ class ImageAdapter( val entry = iterator.next() if (entry.value == image) { imagePositionAsPerIncreasingOrder -= 1 + _currentImagesCount.value = imagePositionAsPerIncreasingOrder iterator.remove() alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1) notifyItemRemoved(index) 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 3912a4d12..39d0d545a 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 @@ -12,8 +12,12 @@ import android.widget.ProgressBar import android.widget.Switch import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import fr.free.nrw.commons.contributions.Contribution @@ -38,6 +42,10 @@ import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch import java.util.TreeMap import javax.inject.Inject import kotlin.collections.ArrayList @@ -80,6 +88,12 @@ class ImageFragment : */ var allImages: ArrayList = ArrayList() + /** + * Keeps track of switch state + */ + private val _switchState = MutableStateFlow(false) + val switchState = _switchState.asStateFlow() + /** * View model Factory. */ @@ -214,7 +228,11 @@ class ImageFragment : switch = binding?.switchWidget switch?.visibility = View.VISIBLE - switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) } + _switchState.value = switch?.isChecked ?: false + switch?.setOnCheckedChangeListener { _, isChecked -> + onChangeSwitchState(isChecked) + _switchState.value = isChecked + } selectorRV = binding?.selectorRv loader = binding?.loader progressLayout = binding?.progressLayout @@ -234,6 +252,28 @@ class ImageFragment : return binding?.root } + /** + * onViewCreated + * Updates empty text view visibility based on image count, switch state, and loading status. + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + combine( + imageAdapter.currentImagesCount, + switchState, + imageAdapter.isLoadingImages + ) { imageCount, isChecked, isLoadingImages -> + Triple(imageCount, isChecked, isLoadingImages) + }.collect { (imageCount, isChecked, isLoadingImages) -> + binding?.allImagesUploadedOrMarked?.isVisible = + !isLoadingImages && !isChecked && imageCount == 0 && (switch?.isVisible == true) + } + } + } + } + private fun onChangeSwitchState(checked: Boolean) { if (checked) { showAlreadyActionedImages = true 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 942946a6b..44cefe4d5 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 @@ -267,11 +267,11 @@ class DescriptionEditActivity : applicationContext, media, mediaDetail.languageCode!!, - mediaDetail.captionText!!, + mediaDetail.captionText, ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { s: Boolean? -> - updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText!! + updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText media.captions = updatedCaptions Timber.d("Caption is added.") }, diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt index b0c0c4d37..94319060b 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt @@ -6,6 +6,7 @@ import dagger.android.AndroidInjectionModule import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.activity.SingleWebViewActivity import fr.free.nrw.commons.auth.LoginActivity import fr.free.nrw.commons.contributions.ContributionsModule import fr.free.nrw.commons.explore.SearchModule @@ -51,6 +52,8 @@ interface CommonsApplicationComponent : AndroidInjector ) { - updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText!! + updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText media!!.captions = updatedCaptions } @@ -1754,10 +1754,11 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C return } ProfileActivity.startYourself( - activity, - media!!.user, - sessionManager.userName != media!!.user + requireActivity(), // Ensure this is a non-null Activity context + media?.user ?: "", // Provide a fallback value if media?.user is null + sessionManager.userName != media?.user // This can remain as is, null check will apply ) + } /** diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java deleted file mode 100644 index 4a08760af..000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ /dev/null @@ -1,265 +0,0 @@ -package fr.free.nrw.commons.profile; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.databinding.ActivityProfileBinding; -import fr.free.nrw.commons.profile.achievements.AchievementsFragment; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DialogUtil; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; - -/** - * This activity will set two tabs, achievements and - * each tab will have their own fragments - */ -public class ProfileActivity extends BaseActivity { - - private FragmentManager supportFragmentManager; - - public ActivityProfileBinding binding; - - @Inject - SessionManager sessionManager; - - private ViewPagerAdapter viewPagerAdapter; - private AchievementsFragment achievementsFragment; - private LeaderboardFragment leaderboardFragment; - - public static final String KEY_USERNAME ="username"; - public static final String KEY_SHOULD_SHOW_CONTRIBUTIONS ="shouldShowContributions"; - - String userName; - private boolean shouldShowContributions; - - ContributionsFragment contributionsFragment; - - public void setScroll(boolean canScroll){ - binding.viewPager.setCanScroll(canScroll); - } - @Override - protected void onRestoreInstanceState(final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - if (savedInstanceState != null) { - userName = savedInstanceState.getString(KEY_USERNAME); - shouldShowContributions = savedInstanceState.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS); - } - } - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityProfileBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbarBinding.toolbar); - - - binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> { - onSupportNavigateUp(); - }); - - userName = getIntent().getStringExtra(KEY_USERNAME); - setTitle(userName); - shouldShowContributions = getIntent().getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false); - - supportFragmentManager = getSupportFragmentManager(); - viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.tabLayout.setupWithViewPager(binding.viewPager); - setTabs(); - } - - /** - * Navigate up event - * @return boolean - */ - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * Creates a way to change current activity to AchievementActivity - * - * @param context - */ - public static void startYourself(final Context context, final String userName, - final boolean shouldShowContributions) { - Intent intent = new Intent(context, ProfileActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(KEY_USERNAME, userName); - intent.putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions); - context.startActivity(intent); - } - - /** - * Set the tabs for the fragments - */ - private void setTabs() { - List fragmentList = new ArrayList<>(); - List titleList = new ArrayList<>(); - achievementsFragment = new AchievementsFragment(); - Bundle achievementsBundle = new Bundle(); - achievementsBundle.putString(KEY_USERNAME, userName); - achievementsFragment.setArguments(achievementsBundle); - fragmentList.add(achievementsFragment); - - titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase()); - leaderboardFragment = new LeaderboardFragment(); - Bundle leaderBoardBundle = new Bundle(); - leaderBoardBundle.putString(KEY_USERNAME, userName); - leaderboardFragment.setArguments(leaderBoardBundle); - - fragmentList.add(leaderboardFragment); - titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); - - contributionsFragment = new ContributionsFragment(); - Bundle contributionsListBundle = new Bundle(); - contributionsListBundle.putString(KEY_USERNAME, userName); - contributionsFragment.setArguments(contributionsListBundle); - fragmentList.add(contributionsFragment); - titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); - - viewPagerAdapter.setTabData(fragmentList, titleList); - viewPagerAdapter.notifyDataSetChanged(); - - } - - @Override - public void onDestroy() { - super.onDestroy(); - getCompositeDisposable().clear(); - } - - /** - * To inflate menu - * @param menu Menu - * @return boolean - */ - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - final MenuInflater menuInflater = getMenuInflater(); - menuInflater.inflate(R.menu.menu_about, menu); - return super.onCreateOptionsMenu(menu); - } - - /** - * To receive the id of selected item and handle further logic for that selected item - * @param item MenuItem - * @return boolean - */ - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - // take screenshot in form of bitmap and show it in Alert Dialog - if (item.getItemId() == R.id.share_app_icon) { - final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); - final Bitmap screenShot = Utils.getScreenShot(rootView); - showAlert(screenShot); - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * It displays the alertDialog with Image of screenshot - * @param screenshot screenshot of the present screen - */ - public void showAlert(final Bitmap screenshot) { - final LayoutInflater factory = LayoutInflater.from(this); - final View view = factory.inflate(R.layout.image_alert_layout, null); - final ImageView screenShotImage = view.findViewById(R.id.alert_image); - screenShotImage.setImageBitmap(screenshot); - final TextView shareMessage = view.findViewById(R.id.alert_text); - shareMessage.setText(R.string.achievements_share_message); - DialogUtil.showAlertDialog(this, - null, - null, - getString(R.string.about_translate_proceed), - getString(R.string.cancel), - () -> shareScreen(screenshot), - () -> {}, - view - ); - } - - /** - * To take bitmap and store it temporary storage and share it - * @param bitmap bitmap of screenshot - */ - void shareScreen(final Bitmap bitmap) { - try { - final File file = new File(getExternalCacheDir(), "screen.png"); - final FileOutputStream fileOutputStream = new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); - fileOutputStream.flush(); - fileOutputStream.close(); - file.setReadable(true, false); - - final Uri fileUri = FileProvider - .getUriForFile(getApplicationContext(), - getPackageName() + ".provider", file); - grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_STREAM, fileUri); - intent.setType("image/png"); - startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); - } catch (final IOException e) { - e.printStackTrace(); - } - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - outState.putString(KEY_USERNAME, userName); - outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions); - super.onSaveInstanceState(outState); - } - - @Override - public void onBackPressed() { - // Checking if MediaDetailPagerFragment is visible, If visible then show ContributionListFragment else close the ProfileActivity - if(contributionsFragment != null && contributionsFragment.getMediaDetailPagerFragment() != null && contributionsFragment.getMediaDetailPagerFragment().isVisible()) { - contributionsFragment.backButtonClicked(); - binding.tabLayout.setVisibility(View.VISIBLE); - }else { - super.onBackPressed(); - } - } - - /** - * To set the visibility of tab layout - * @param isVisible boolean - */ - public void setTabLayoutVisibility(boolean isVisible) { - binding.tabLayout.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt new file mode 100644 index 000000000..164842c9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt @@ -0,0 +1,229 @@ +package fr.free.nrw.commons.profile + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.* +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.databinding.ActivityProfileBinding +import fr.free.nrw.commons.profile.achievements.AchievementsFragment +import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.DialogUtil +import java.io.File +import java.io.FileOutputStream +import java.util.* +import javax.inject.Inject + +/** + * This activity will set two tabs, achievements and + * each tab will have their own fragments + */ +class ProfileActivity : BaseActivity() { + + lateinit var binding: ActivityProfileBinding + + @Inject + lateinit var sessionManager: SessionManager + + private lateinit var viewPagerAdapter: ViewPagerAdapter + private lateinit var achievementsFragment: AchievementsFragment + private lateinit var leaderboardFragment: LeaderboardFragment + private lateinit var userName: String + private var shouldShowContributions: Boolean = false + private var contributionsFragment: ContributionsFragment? = null + + fun setScroll(canScroll: Boolean) { + binding.viewPager.setCanScroll(canScroll) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + savedInstanceState.let { + userName = it.getString(KEY_USERNAME, "") + shouldShowContributions = it.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityProfileBinding.inflate(layoutInflater) + setContentView(binding.root) + setSupportActionBar(binding.toolbarBinding.toolbar) + + binding.toolbarBinding.toolbar.setNavigationOnClickListener { + onSupportNavigateUp() + } + + userName = intent.getStringExtra(KEY_USERNAME) ?: "" + title = userName + shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false) + + viewPagerAdapter = ViewPagerAdapter(supportFragmentManager) + binding.viewPager.adapter = viewPagerAdapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + setTabs() + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + private fun setTabs() { + val fragmentList = mutableListOf() + val titleList = mutableListOf() + + // Add Achievements tab + achievementsFragment = AchievementsFragment().apply { + arguments = Bundle().apply { + putString(KEY_USERNAME, userName) + } + } + fragmentList.add(achievementsFragment) + titleList.add(resources.getString(R.string.achievements_tab_title).uppercase()) + + // Add Leaderboard tab + leaderboardFragment = LeaderboardFragment().apply { + arguments = Bundle().apply { + putString(KEY_USERNAME, userName) + } + } + fragmentList.add(leaderboardFragment) + titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT)) + + // Add Contributions tab + contributionsFragment = ContributionsFragment().apply { + arguments = Bundle().apply { + putString(KEY_USERNAME, userName) + } + } + contributionsFragment?.let { + fragmentList.add(it) + titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT)) + } + + viewPagerAdapter.setTabData(fragmentList, titleList) + viewPagerAdapter.notifyDataSetChanged() + } + + public override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_about, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.share_app_icon -> { + val rootView = window.decorView.findViewById(android.R.id.content) + val screenShot = Utils.getScreenShot(rootView) + if (screenShot == null) { + Log.e("ERROR", "ScreenShot is null") + return false + } + showAlert(screenShot) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + fun showAlert(screenshot: Bitmap) { + val view = layoutInflater.inflate(R.layout.image_alert_layout, null) + val screenShotImage = view.findViewById(R.id.alert_image) + val shareMessage = view.findViewById(R.id.alert_text) + + screenShotImage.setImageBitmap(screenshot) + shareMessage.setText(R.string.achievements_share_message) + + DialogUtil.showAlertDialog( + this, + null, + null, + getString(R.string.about_translate_proceed), + getString(R.string.cancel), + { shareScreen(screenshot) }, + {}, + view + ) + } + + private fun shareScreen(bitmap: Bitmap) { + try { + val file = File(externalCacheDir, "screen.png") + FileOutputStream(file).use { out -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + out.flush() + } + file.setReadable(true, false) + + val fileUri = FileProvider.getUriForFile( + applicationContext, + "$packageName.provider", + file + ) + + grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + + val intent = Intent(Intent.ACTION_SEND).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(Intent.EXTRA_STREAM, fileUri) + type = "image/png" + } + startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))) + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(KEY_USERNAME, userName) + outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions) + } + + override fun onBackPressed() { + if (contributionsFragment?.mediaDetailPagerFragment?.isVisible == true) { + contributionsFragment?.backButtonClicked() + binding.tabLayout.visibility = View.VISIBLE + } else { + super.onBackPressed() + } + } + + fun setTabLayoutVisibility(isVisible: Boolean) { + binding.tabLayout.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + companion object { + const val KEY_USERNAME = "username" + const val KEY_SHOULD_SHOW_CONTRIBUTIONS = "shouldShowContributions" + + @JvmStatic + fun startYourself(context: Context, userName: String, shouldShowContributions: Boolean) { + val intent = Intent(context, ProfileActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + putExtra(KEY_USERNAME, userName) + putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions) + } + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt index c7bccf950..99de68f80 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt @@ -58,7 +58,7 @@ class LeaderboardListAdapter : PagedListAdapter if (view.context is ProfileActivity) { ((view.context) as Activity).finish() } - ProfileActivity.startYourself(view.context, item.username, true) + ProfileActivity.startYourself(view.context, item.username?:"", true) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt index 3cb4f52a6..d9b6b7e52 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt @@ -14,11 +14,15 @@ class QuizController { private val quiz: ArrayList = ArrayList() - private val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg" - private val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg" - private val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg" - private val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png" - private val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg" + companion object{ + + const val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg" + const val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg" + const val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg" + const val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png" + const val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg" + } + fun initialize(context: Context) { val q1 = QuizQuestion( diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt index 40eb24ed0..209f991fb 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -45,12 +45,12 @@ class ReviewActivity : BaseActivity() { private var hasNonHiddenCategories = false var media: Media? = null - private val SAVED_MEDIA = "saved_media" + private val savedMedia = "saved_media" override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) media?.let { - outState.putParcelable(SAVED_MEDIA, it) + outState.putParcelable(savedMedia, it) } } @@ -90,8 +90,8 @@ class ReviewActivity : BaseActivity() { PorterDuff.Mode.SRC_IN ) - if (savedInstanceState?.getParcelable(SAVED_MEDIA) != null) { - updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!) + if (savedInstanceState?.getParcelable(savedMedia) != null) { + updateImage(savedInstanceState.getParcelable(savedMedia)!!) setUpMediaDetailOnOrientation() } else { runRandomizer() diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt index f6da76bba..2bc333505 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt @@ -31,7 +31,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() { lateinit var sessionManager: SessionManager // Constant variable used to store user's key name for onSaveInstanceState method - private val SAVED_USER = "saved_user" + private val savedUser = "saved_user" // Variable that stores the value of user private var user: String? = null @@ -129,7 +129,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() { question = getString(R.string.review_thanks) user = reviewActivity.reviewController.firstRevision?.user() - ?: savedInstanceState?.getString(SAVED_USER) + ?: savedInstanceState?.getString(savedUser) //if the user is null because of whatsoever reason, review will not be sent anyways if (!user.isNullOrEmpty()) { @@ -172,7 +172,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) //Save user name when configuration changes happen - outState.putString(SAVED_USER, user) + outState.putString(savedUser, user) } private val reviewCallback: ReviewController.ReviewCallback diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index 2f293937c..161927d03 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -33,6 +33,7 @@ import com.karumi.dexter.MultiplePermissionsReport import com.karumi.dexter.PermissionToken import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import fr.free.nrw.commons.BuildConfig.MOBILE_META_URL import fr.free.nrw.commons.R import fr.free.nrw.commons.Utils import fr.free.nrw.commons.activity.SingleWebViewActivity @@ -85,7 +86,6 @@ class SettingsFragment : PreferenceFragmentCompat() { private var languageHistoryListView: ListView? = null private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher> - private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content" private val cameraPickLauncherForResult: ActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { result -> @@ -271,6 +271,7 @@ class SettingsFragment : PreferenceFragmentCompat() { findPreference("managed_exif_tags")?.isEnabled = false findPreference("openDocumentPhotoPickerPref")?.isEnabled = false findPreference("inAppCameraLocationPref")?.isEnabled = false + findPreference("vanishAccount")?.isEnabled = false } } @@ -511,6 +512,7 @@ class SettingsFragment : PreferenceFragmentCompat() { @Suppress("LongLine") companion object { + const val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content" private const val VANISH_ACCOUNT_URL = "https://meta.m.wikimedia.org/wiki/Special:Contact/accountvanishapps" private const val VANISH_ACCOUNT_SUCCESS_URL = "https://meta.m.wikimedia.org/wiki/Special:GlobalVanishRequest/vanished" /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt index ba9dc147c..fca10be1e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt @@ -10,7 +10,6 @@ import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK import fr.free.nrw.commons.utils.ImageUtilsWrapper import io.reactivex.Single -import io.reactivex.functions.Function import io.reactivex.schedulers.Schedulers import org.apache.commons.lang3.StringUtils import timber.log.Timber @@ -26,7 +25,7 @@ class ImageProcessingService @Inject constructor( private val fileUtilsWrapper: FileUtilsWrapper, private val imageUtilsWrapper: ImageUtilsWrapper, private val readFBMD: ReadFBMD, - private val EXIFReader: EXIFReader, + private val exifReader: EXIFReader, private val mediaClient: MediaClient ) { /** @@ -94,7 +93,7 @@ class ImageProcessingService @Inject constructor( * the presence of some basic Exif metadata. */ private fun checkEXIF(filepath: String): Single = - EXIFReader.processMetadata(filepath) + exifReader.processMetadata(filepath) /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt index f21c48db7..6a516537f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt @@ -706,17 +706,64 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C private fun receiveInternalSharedItems() { val intent = intent + Timber.d("Intent has EXTRA_FILES: ${EXTRA_FILES}") + uploadableFiles = try { + // Check if intent has the extra before trying to read it + if (!intent.hasExtra(EXTRA_FILES)) { + Timber.w("No EXTRA_FILES found in intent") + mutableListOf() + } else { + // Try to get the files as Parcelable array + val files = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra(EXTRA_FILES, UploadableFile::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayListExtra(EXTRA_FILES) + } - Timber.d("Received intent %s with action %s", intent.toString(), intent.action) - - uploadableFiles = mutableListOf().apply { - addAll(intent.getParcelableArrayListExtra(EXTRA_FILES) ?: emptyList()) + // Convert to mutable list or return empty list if null + files?.toMutableList() ?: run { + Timber.w("Files array was null") + mutableListOf() + } + } + } catch (e: Exception) { + Timber.e(e, "Error reading files from intent") + mutableListOf() + } + + // Log the result for debugging + isMultipleFilesSelected = uploadableFiles.size > 1 + Timber.i("Received files count: ${uploadableFiles.size}") + uploadableFiles.forEachIndexed { index, file -> + Timber.d("File $index path: ${file.getFilePath()}") + } + + // Handle other extras with null safety + place = try { + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(PLACE_OBJECT, Place::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(PLACE_OBJECT) + } + } catch (e: Exception) { + Timber.e(e, "Error reading place") + null + } + + prevLocation = try { + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE, LatLng::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE) + } + } catch (e: Exception) { + Timber.e(e, "Error reading location") + null } - isMultipleFilesSelected = uploadableFiles.size > 1 - Timber.i("Received multiple upload %s", uploadableFiles.size) - place = intent.getParcelableExtra(PLACE_OBJECT) - prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE) isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false) resetDirectPrefs() } @@ -826,6 +873,21 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C onBackPressedCallback.remove() } + /** + * Overrides the back button to make sure the user is prepared to lose their progress + */ + @SuppressLint("MissingSuperCall") + override fun onBackPressed() { + showAlertDialog( + this, + getString(R.string.back_button_warning), + getString(R.string.back_button_warning_desc), + getString(R.string.back_button_continue), + getString(R.string.back_button_warning), + null + ) { finish() } + } + /** * If the user uploads more than 1 file informs that * depictions/categories apply to all pictures of a multi upload. @@ -933,7 +995,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C companion object { private var uploadIsOfAPlace = false - const val EXTRA_FILES: String = "commons_image_exta" + const val EXTRA_FILES: String = "commons_image_extra" const val LOCATION_BEFORE_IMAGE_CAPTURE: String = "user_location_before_image_capture" const val IN_APP_CAMERA_UPLOAD: String = "in_app_camera_upload" diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt index a2b3168ec..351e53124 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetail.kt @@ -23,14 +23,14 @@ data class UploadMediaDetail( * The caption text for the item being uploaded. * @param captionText The caption text. */ - var captionText: String? = "", + var captionText: String = "", ) : Parcelable { fun javaCopy() = copy() constructor(place: Place?) : this( place?.language, place?.longDescription, - place?.name, + place?.name ?: "", ) /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 7536cad75..dbeeae6ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -140,7 +140,7 @@ class CategoriesPresenter */ private fun getImageTitleList(): List = repository.getUploads() - .map { it.uploadMediaDetails[0].captionText!! } + .map { it.uploadMediaDetails[0].captionText } .filterNot { TextUtils.isEmpty(it) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt index 0e4ed482d..4ae366535 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictedItem.kt @@ -68,6 +68,9 @@ data class DepictedItem constructor( entity.id(), ) + val primaryImage: String? + get() = imageUrl?.split('-')?.lastOrNull() + override fun equals(other: Any?) = when { this === other -> true diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt index daf158fc1..df3b33bf6 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt @@ -16,6 +16,9 @@ import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.multi.MultiplePermissionsListener import fr.free.nrw.commons.R import fr.free.nrw.commons.upload.UploadActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch object PermissionUtils { @@ -130,7 +133,7 @@ object PermissionUtils { vararg permissions: String ) { if (hasPartialAccess(activity)) { - Thread(onPermissionGranted).start() + CoroutineScope(Dispatchers.Main).launch { onPermissionGranted.run() } return } checkPermissionsAndPerformAction( @@ -166,13 +169,15 @@ object PermissionUtils { rationaleMessage: Int, vararg permissions: String ) { + val scope = CoroutineScope(Dispatchers.Main) + Dexter.withActivity(activity) .withPermissions(*permissions) .withListener(object : MultiplePermissionsListener { override fun onPermissionsChecked(report: MultiplePermissionsReport) { when { report.areAllPermissionsGranted() || hasPartialAccess(activity) -> - Thread(onPermissionGranted).start() + scope.launch { onPermissionGranted.run() } report.isAnyPermissionPermanentlyDenied -> { DialogUtil.showAlertDialog( activity, @@ -189,7 +194,7 @@ object PermissionUtils { null, null ) } - else -> Thread(onPermissionDenied).start() + else -> scope.launch { onPermissionDenied?.run() } } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt index 5a8261c24..bde575386 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt @@ -21,9 +21,14 @@ abstract class SwipableCardView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : CardView(context, attrs, defStyleAttr) { + + companion object{ + const val MINIMUM_THRESHOLD_FOR_SWIPE = 100f + } + private var x1 = 0f private var x2 = 0f - private val MINIMUM_THRESHOLD_FOR_SWIPE = 100f + init { interceptOnTouchListener() diff --git a/app/src/main/res/layout/fragment_achievements.xml b/app/src/main/res/layout/fragment_achievements.xml index 00c18b323..c688f983a 100644 --- a/app/src/main/res/layout/fragment_achievements.xml +++ b/app/src/main/res/layout/fragment_achievements.xml @@ -8,7 +8,7 @@ android:fillViewport="true" tools:ignore="ContentDescription" > - + + كومنز مواقع ويكي أخرى حالات استخدام الملف + نشاط عرض ويب واحد + حساب + حذف الحساب + تحذير من اختفاء الحساب + الاختفاء هو <b>الملاذ الأخير</b> ويجب <b>استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد</b> وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.<br/><br/> يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. <b>لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع</b> . الشرح تم نسخ التسمية التوضيحية إلى الحافظة diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 12eebaa7d..cd974dc30 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -708,6 +708,7 @@ 다른 위키 이 파일을 사용하는 문서 계정 + 계정 버리기 캡션 캡션이 클립보드에 복사되었습니다 diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index f43975c21..52a1243aa 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -109,7 +109,7 @@ Сиздин сүрөттөр дүйнө жүзүндөгү адамдардын билим алышына өбөлгө түзүүдө. Интернетте жарыяланган автордук укукка ээ сүрөттөрдөн, ошондой эле плакаттардан жана китептердин мукабасынан ж.б. четтеңиз. Сизге бул түшүнүктүүбү? - Ооба ! + Ооба! Категориялар Жүктөлүүдө… Тандалган жок diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 354c44b5c..1aaee6e9c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -831,4 +831,8 @@ Commons Andere wiki’s Bestandsgebruik + Activiteit enkele webraadpleging + Account + Account laten verdwijnen + Waarschuwing verwijdering account diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f016f5f39..be06b0b99 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -813,7 +813,11 @@ Commons Andra wikier Filanvändning + SingleWebViewActivity Konto + Få kontot att försvinna + Varning om försvinnande konto + Att få kontot att försvinna är en <b>sista utväg</b> och bör <b>endast användas när du vill sluta redigera för alltid</b> och även dölja så många av dina tidigare associationer som möjligt.<br/><br/>Konton raderas på Wikimedia Commons genom att ändra kontonamnet för att göra så att andra inte kan känna igen bidragen i en process som kallas kontoförsvinnande. <b>Försvinnande garanterar inte fullständig anonymitet eller att bidrag tas bort från projekten</b>. Bildtext Bildtext kopierades till urklipp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7504df98..d2bde98ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -867,5 +867,6 @@ Upload your first media by tapping on the add button. last resort and should only be used when you wish to stop editing forever and also to hide as many of your past associations as possible.

Account deletion on Wikimedia Commons is done by changing your account name to make it so others cannot recognize your contributions in a process called account vanishing. Vanishing does not guarantee complete anonymity or remove contributions to the projects.]]>
Caption Caption copied to clipboard + Congratulations, all pictures in this album have been either uploaded or marked as not for upload. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 67b5eae0f..02c314a4a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -20,6 +20,7 @@ @color/white @color/white @color/white + @color/main_background_dark @color/commons_app_blue_dark @color/sub_background_dark diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt index cc5340fbb..8c336470a 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoriesModelTest.kt @@ -1,7 +1,9 @@ package fr.free.nrw.commons.category import categoryItem +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import depictedItem @@ -90,14 +92,18 @@ class CategoriesModelTest { val depictedItem = depictedItem( commonsCategories = - listOf( - CategoryItem( - "depictionCategory", - "", - "", - false, - ), + listOf( + CategoryItem( + "depictionCategory", + "", + "", + false, ), + ), + ) + val depictedItemWithoutCategories = + depictedItem( + imageUrl = "testUrl" ) whenever(gpsCategoryModel.categoriesFromLocation) @@ -159,6 +165,23 @@ class CategoriesModelTest { ), ), ) + whenever( + categoryClient.getCategoriesOfImage( + "testUrl", + 25, + ), + ).thenReturn( + Single.just( + listOf( + CategoryItem( + "categoriesOfP18", + "", + "", + false, + ), + ), + ), + ) val imageTitleList = listOf("Test") CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) .searchAll("", imageTitleList, listOf(depictedItem)) @@ -171,8 +194,21 @@ class CategoriesModelTest { categoryItem("recentCategories"), ), ) + CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) + .searchAll("", imageTitleList, listOf(depictedItemWithoutCategories)) + .test() + .assertValue( + listOf( + categoryItem("categoriesOfP18"), + categoryItem("gpsCategory"), + categoryItem("titleSearch"), + categoryItem("recentCategories"), + ), + ) imageTitleList.forEach { - verify(categoryClient).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT) + verify(categoryClient, times(2)).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT) + verify(categoryClient).getCategoriesByName(any(), any(), any(), any()) + verify(categoryClient).getCategoriesOfImage(any(), any()) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt index 5c95215a8..5edf55f6e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryClientTest.kt @@ -132,6 +132,45 @@ class CategoryClientTest { ) } + @Test + fun getCategoriesByTitlesFound() { + val mockResponse = withMockResponse("Category:Test") + whenever( + categoryInterface.getCategoriesByTitles( + anyString(), + anyInt(), + ), + ).thenReturn(Single.just(mockResponse)) + categoryClient + .getCategoriesOfImage("tes", 10) + .test() + .assertValues( + listOf( + CategoryItem( + "Test", + "", + "", + false, + ), + ), + ) + categoryClient + .getCategoriesOfImage( + "tes", + 10, + ).test() + .assertValues( + listOf( + CategoryItem( + "Test", + "", + "", + false, + ), + ), + ) + } + @Test fun getCategoriesByNameNull() { val mockResponse = withNullPages() @@ -160,6 +199,29 @@ class CategoryClientTest { .assertValues(emptyList()) } + @Test + fun getCategoriesByTitlesNull() { + val mockResponse = withNullPages() + whenever( + categoryInterface.getCategoriesByTitles( + anyString(), + anyInt(), + ), + ).thenReturn(Single.just(mockResponse)) + categoryClient + .getCategoriesOfImage( + "tes", + 10, + ).test() + .assertValues(emptyList()) + categoryClient + .getCategoriesOfImage( + "tes", + 10, + ).test() + .assertValues(emptyList()) + } + @Test fun getParentCategoryListFound() { val mockResponse = withMockResponse("Category:Test") diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt index e0d339eee..faec52051 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt @@ -181,4 +181,20 @@ class DepictedItemTest { fun `hashCode returns different values for objects with different name`() { Assert.assertNotEquals(depictedItem(name = "a").hashCode(), depictedItem(name = "b").hashCode()) } + + @Test + fun `primaryImage is derived correctly from imageUrl`() { + Assert.assertEquals( + DepictedItem( + entity( + statements = mapOf( + WikidataProperties.IMAGE.propertyName to listOf( + statement(snak(dataValue = valueString("prefix: example_image name"))), + ), + ), + ), + ).primaryImage, + "_example_image_name", + ) + } }